Architecture overview
Dialer V2 is a single Next.js application. There is no separate backend service: HTTP route handlers, business logic, database access and durable workflow definitions all live in the same deployment, with long-running or retryable work delegated to Inngest and downstream queues.
System context
flowchart LR
User["Operator (browser)"] --> App
subgraph App["Dialer V2 — Next.js on Vercel"]
Pages["Dashboard pages"]
Api["Route handlers /api/*"]
Inngest["Inngest functions"]
Mcp["MCP tool server"]
end
App --> PG[("Supabase PostgreSQL")]
App --> Auth["Supabase Auth"]
App --> Redis[("Redis")]
App --> BQ[("BigQuery")]
App --> SQS["AWS SQS"]
App --> Gateway["Vercel AI Gateway"]
App --> Retell["Retell AI"]
App --> Resend["Resend"]
App --> HubSpot["HubSpot"]
App --> MsgApp["MessagingApp\n(number purchase)"]
Retell -- webhooks --> Api
Resend -- webhooks --> Api
HubSpot -- webhooks --> Api
Nooks["Nooks (human dialer)"] -- webhooks --> Api
Cron["pg_cron / Inngest cron"] --> Api
Orchestrators["External AI orchestrators"] --> Mcp
Containers and responsibilities
| Container | Responsibility |
|---|---|
Dashboard (React Server + Client Components) |
Renders the operator UI. All server state flows through React Query hooks that call the app’s own HTTP API — pages never query Prisma directly. See Frontend. |
Route handlers ( |
The only HTTP surface. Authenticate, validate, delegate, serialise. No business logic, no Prisma. See API reference. |
Services ( |
All business decisions: eligibility, selection, caps, scoring, state transitions. |
Repositories ( |
The only code allowed to import Prisma. Own every query, index usage and transaction. |
Event handlers ( |
Turn provider webhooks into database changes and follow-up events, through per-provider dispatcher registries. See Events. |
Inngest functions ( |
Durable, step-wise, retryable work: dialing, post-call analysis, email sending, sequence execution, number purchase. See Workflows. |
MCP server ( |
A thin JSON-RPC 2.0 tool layer over the same services, for external AI orchestrators. |
Guiding decisions
- Three-layer discipline
-
Routes orchestrate HTTP, services decide, repositories persist. This is enforced by convention and review rather than tooling, and it is what makes the dialer’s rules testable in isolation. See Layering rules.
- Tenant scoping is not optional
-
Every tenant-scoped query filters on
subscriptionId, resolved once per request byresolveSubscription(). See Multi-tenancy. - Webhooks acknowledge fast, work later
-
An inbound webhook persists the raw payload, updates the minimum state needed, and emits an Inngest event for anything expensive. This keeps provider retries rare and makes the heavy work replayable. See Asynchronous runtime.
- Registries pattern for extension points
-
Webhook event types, MCP tools, CRM event types and carrier providers are all registered in a single map so that adding one is a one-file change. See Adding a handler and MCP tool server.
- Schema-first types
-
A Zod schema is the source of truth; TypeScript types are
z.infer<>of it, and API responses are parsed through the schema in the React Query hook so schema drift surfaces immediately. - Idempotency by construction
-
WebhookEventis unique on(source, eventId);Call.providerCallIdis unique; theemailSendworkflow refuses to re-send a target that has leftpending. Replays are safe.