- Industry
- Enterprise IVR · contact-center operations · QA
- Product
- Authorized IVR validation & telephony test console
- Users
- Operators, backend service, Twilio (webhooks)
- Stack
- Next.js · NestJS · Prisma · Postgres · Twilio SDK
- Proof
- 3 sanitized screenshots · OpenAPI · health preflight JSON
The problem
IVR validation is usually manual: an engineer dials, waits through prompts, keys a test value, then reconstructs what happened from the Twilio console. That workflow is opaque, hard to audit, and easy to mis-time—especially when post-answer settle and pre-menu wait are conflated.
Constraints
Delivery stayed inside the client's stack, tenancy, compliance and media-ownership boundaries. Where a choice was forced (suite vs owned plane, BYOC vs CPaaS, local vs cloud models), the architecture section below records the trade-off rather than a marketing rewrite.
Enterprises need proof that a known DTMF value can be entered at the right moment on an already-approved destination, with durable records: who started the run, which line was used, Call SID, digits sent, and how the call ended.
The solution
Three ownership domains stay separate:
- Twilio — originate, play DTMF via TwiML, report status.
- PostgreSQL — runs, attempts, line
AVAILABLE/BUSY,call_events, results. - NestJS —
POST /test-runs,calls.create, TwiML builder, webhook state machine. - Next.js — health strip, timing fields, Start/Refresh, polling while active.
Starting a test is one REST call; everything after is webhook-driven. The destination IVR number is server-side only (TWILIO_TEST_NUMBER).
Architecture
flowchart TB UI["Next.js operator console"] -->|"REST"| API["NestJS API"] API --> DB["PostgreSQL 16"] API -->|"calls.create"| TW["Twilio Voice"] TW --> IVR["Authorized IVR"] TW -->|"POST /twilio/*"| API
Operator console

Dark operational UI: four health pills (backend, Postgres, Twilio, webhook URL), explicit TwiML timing fields, masked DID with line state, summary counts, and a stored result panel. Stop/Resume are Milestone 2 placeholders; Start and Refresh are live in M1.
Evidence & API surface

OpenAPI at /api documents operator REST. Twilio consumes /twilio/voice and /twilio/status with signature validation—not the browser.

GET /health exposes ready: true only when Postgres, Twilio env, and public webhook base URL are all present—plus effective TwiML pause values in milliseconds.
Call & webhook workflow
sequenceDiagram participant UI as Next.js participant API as NestJS participant DB as PostgreSQL participant Tw as Twilio participant IVR as Authorized IVR UI->>API: POST /test-runs API->>DB: run RUNNING line BUSY API->>Tw: calls.create Tw->>IVR: outbound voice Tw->>API: POST /twilio/voice API-->>Tw: TwiML pause Play digits pause Hangup Tw->>API: POST /twilio/status API->>DB: SUCCESS release line UI->>API: GET /test-runs/:id
TwiML order: settle (default 2s) → pre-DTMF wait (default 40s) → Play digits= → between-values gap (2s) → listen window (15s) → hangup. Status callbacks map completed to attempt SUCCESS and release the line; terminal failures release the line even when calls.create fails.
Milestone 1 boundaries
- In scope — one line, one attempt per Start, single DTMF value, webhook audit, Docker Compose stack.
- Out of scope (later) — multi-line workers, transcription, winner detection, Stop/Resume, app login/JWT.
Outcomes
- Repeatable authorized IVR test with Postgres audit trail—not console archaeology.
- Timing as configuration (settle vs pre-DTMF wait) exposed in health and UI.
- Signature-validated telephony callbacks stored as immutable
call_events. - First-class
phone_linesresource models BUSY/AVAILABLE for future concurrency. - Compose-ready stack: web :3000, API :3001, Postgres :5432, migrate + seed on boot.
No conversion or cost metrics are claimed; Milestone 1 proves the vertical slice for stakeholder demos and QA handoff.
FAQ — buyer & architecture questions
Ten common questions about this case study, fit, and engagement.