Freelance engineering projects rarely fail because of code alone.
Sometimes the architecture works. The deployment works. The integrations work. The milestone is delivered.
And then the final testing requirement changes.
This is a real-world lesson from a recent automation project involving Twilio, IVR automation, DTMF, NestJS, Next.js, PostgreSQL, Docker, and cloud deployment.
The experience was mixed.
Technically, it was a successful delivery.
Professionally, it reinforced why engineers must maintain clear boundaries around scope, testing, authorization, security, and ethics.
The Original Engineering Challenge
The project required building an automated IVR testing platform.
The core architecture involved:
- Twilio outbound voice calls
- Automated IVR interaction
- Configurable DTMF timing
- Webhook processing
- Call-status tracking
- PostgreSQL persistence
- Dashboard visibility
- Docker deployment
- Cloud VPS infrastructure
- Logging and error handling
The agreed Milestone 1 objective was essentially:
Outbound call → IVR interaction → DTMF input → webhook/status events → PostgreSQL persistence → logs
The implementation stack included:
Next.js Dashboard
↓
NestJS API
↓
PostgreSQL
↓
Twilio Voice API
↓
IVR
↓
Voice + Status WebhooksSimple on paper.
More interesting in production.
Challenge 1: Timing Is Part of the Architecture
One of the biggest technical challenges was that IVR automation is not just about sending DTMF digits.
Timing matters.
The IVR needed:
Call connects
↓
Wait for IVR initialization
↓
Wait for the required menu stage
↓
Send DTMF
↓
Listen for the response
↓
Process webhooksThe timing configuration was made explicit:
- IVR settle delay
- Pre-DTMF wait
- Delay between values
- Post-DTMF listening window
This was important because hardcoding timing assumptions makes IVR automation fragile.
Instead, the system was designed around configurable timing parameters.
Challenge 2: Webhooks Must Be Treated as Unreliable Distributed Events
A major engineering consideration was webhook handling.
A call may generate:
- voice callbacks
- ringing events
- answered events
- in-progress events
- completed events
- duplicate events
- events arriving in unexpected order
Therefore, the state machine had to prevent invalid transitions.
For example:
PENDING
↓
ASSIGNED
↓
CALLING
↓
WAITING
↓
DTMF_SENT
↓
SUCCESSA completed webhook should not accidentally cause an already-successful attempt to be processed again.
Likewise:
DTMF_SENTshould not regress back to:
CALLINGThis required forward-only state handling and idempotent webhook processing.
Challenge 3: Persistence Is More Important Than UI State
The dashboard should display the system state.
It should not be the system state.
The important data was persisted in PostgreSQL:
- test runs
- attempts
- Twilio Call SID
- webhook events
- timestamps
- statuses
- results
- errors
That meant the system could be inspected and validated independently of the frontend.
A successful automation system needs an audit trail.
For external integrations, logs alone are not enough.
Challenge 4: Deployment Problems Are Real Engineering Work
The application was deployed to a client-provided Ubuntu VPS.
The deployment included:
- SSH access configuration
- Docker
- Docker Compose
- PostgreSQL
- NestJS production build
- Next.js application
- environment configuration
- service health checks
The dashboard was eventually exposed directly through:
http://SERVER-IP/rather than requiring:
http://SERVER-IP:3000The deployment also required verifying:
Backend → Healthy
PostgreSQL → Healthy
Twilio Configuration → Valid
Webhook Configuration → Ready
Phone Line → AvailableThis became an important lesson:
Deployment is not the final step after engineering. Deployment is part of engineering.
Challenge 5: HTTPS Requires the Right Infrastructure
Twilio webhooks need publicly reachable HTTPS endpoints.
A bare server IP address creates a practical limitation.
A trusted TLS certificate generally requires a domain name under appropriate DNS control.
The deployment therefore required separating:
- application hosting
- public routing
- webhook exposure
- TLS certificates
- DNS
For development and testing, a secure HTTPS tunnel can be useful.
For production, a domain with:
DNS
↓
Nginx / Reverse Proxy
↓
Let's Encrypt TLS
↓
Application APIis a cleaner architecture.
The Validation Strategy
Before attempting a live integration test, I created a preflight process.
The preflight checked:
- API health
- PostgreSQL connectivity
- Twilio credentials
- webhook configuration
- timing configuration
- phone-line availability
The result was:
PREFLIGHT PASSAdditional verification included:
- API unit tests
- production build
- dashboard type checking
- Docker health checks
- PostgreSQL readiness
The principle was straightforward:
Do not spend money or consume external resources testing a live integration until everything that can be validated locally has already passed.
The Mixed Part of the Experience
This is where the project became a professional case study rather than just another deployment.
The original milestone was delivered and deployed.
The system was ready for the agreed validation flow.
Later, additional requirements were introduced around sequential testing and persistent progress across multiple calls.
Requirement changes are normal.
They happen in real projects.
The important issue is how those changes are handled.
A new requirement can affect:
- architecture
- database design
- state management
- scheduling
- retry behavior
- persistence
- acceptance criteria
- milestone scope
The key lesson:
A clarification is not always a clarification. Sometimes it is a new feature expressed as a testing requirement.
When Automation Needs a Boundary
Automation engineers solve difficult problems.
But not every technically possible automation should be implemented.
In this project, the final testing discussion moved toward functionality that I was not comfortable executing against a real financial system.
At that point, the correct engineering decision was not to optimize the algorithm.
The correct decision was to stop and reassess the environment and authorization boundary.
A responsible automation engineer should ask:
- Is this a genuine sandbox?
- Is the data synthetic?
- Is the target explicitly designed for automated testing?
- Is the requested behavior authorized?
- Does the automation create security or fraud risk?
- Is the acceptance criteria materially different from the agreed scope?
If the answers are unclear, pause.
That is not a failure to deliver.
That is professional engineering judgment.
The Biggest Lesson: Scope Control Is a Technical Skill
We often think of scope management as project management.
It is also an engineering discipline.
Consider the difference:
Initial requirement
One test call
→ One configured DTMF value
→ Webhook events
→ Database persistence
→ LogsExpanded requirement
Persistent sequence
→ Multiple values
→ Multiple calls
→ Resume from next untested value
→ Prevent duplicate attempts
→ Detect success
→ Stop all workers
→ Save winning result
→ Recover after restartThose are not equivalent systems.
The second introduces:
- schedulers
- persistent cursors
- concurrency control
- restart recovery
- transactional state transitions
- duplicate prevention
- global stop conditions
That is a different level of system complexity.
What I Delivered
The delivered Milestone 1 system included:
- Twilio integration
- Automated outbound call initiation
- IVR timing configuration
- DTMF automation
- Voice webhooks
- Status webhooks
- PostgreSQL persistence
- Call event logging
- State handling
- Error handling
- Docker deployment
- VPS deployment
- Production build validation
- Health checks
- Preflight validation
- E2E validation procedure
The application was deployed on the client-provided infrastructure and prepared for authorized environment testing.
What I Would Do Differently Next Time
1. Freeze acceptance criteria earlier
Before coding:
Milestone
Scope
Inputs
Outputs
Test environment
Acceptance criteria
Out-of-scope itemsshould all be documented.
2. Separate sandbox credentials from production credentials
The environment should explicitly state:
SANDBOX ONLYand use dedicated synthetic data.
3. Require test-environment details before milestone acceptance testing
Do not leave the final validation environment undefined.
4. Define scope-change rules
For example:
Any feature requiring new database state, new UI controls, new persistence logic, scheduling, or workflow changes is evaluated separately as a scope change.
5. Build preflight validation first
Before live testing:
Configuration ✓
Database ✓
API ✓
External credentials ✓
Webhook ✓
Health ✓Only then trigger external activity.
Final Takeaway
This project was a mixed experience.
Technically, it was valuable.
The architecture was built, the application was deployed, the integration was hardened, and the milestone deliverables were completed.
The experience also reinforced something more important than any framework or cloud tool:
Good engineers do not simply automate every instruction they receive.
We validate.
We question.
We document.
We define boundaries.
And when a requirement moves outside a safe or acceptable boundary, we stop rather than blindly executing it.
That is not just risk management.
That is professional engineering.
Technologies Used
Twilio · NestJS · Next.js · PostgreSQL · Docker · Docker Compose · Ubuntu · Webhooks · REST APIs · DTMF · IVR Automation · VPS Deployment · SSH · Nginx · Let's Encrypt
