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 (app/api/**/route.ts)

The only HTTP surface. Authenticate, validate, delegate, serialise. No business logic, no Prisma. See API reference.

Services (lib/services, lib/ai)

All business decisions: eligibility, selection, caps, scoring, state transitions.

Repositories (lib/repositories)

The only code allowed to import Prisma. Own every query, index usage and transaction.

Event handlers (lib/events)

Turn provider webhooks into database changes and follow-up events, through per-provider dispatcher registries. See Events.

Inngest functions (lib/workflows)

Durable, step-wise, retryable work: dialing, post-call analysis, email sending, sequence execution, number purchase. See Workflows.

MCP server (lib/mcp)

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 by resolveSubscription(). 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

WebhookEvent is unique on (source, eventId); Call.providerCallId is unique; the emailSend workflow refuses to re-send a target that has left pending. Replays are safe.