Skip to content

Avoca Architecture — Sandy's Working Notes

A living document. Sandy's evolving understanding of Avoca's system, layered on top of their captured engineering onboarding. Quotes from Avoca's docs are blockquoted. Sandy's interpretations and questions are in ::: info callouts. Diagrams are Sandy's mental model — not Avoca's official architecture.


How to read this doc

ConventionMeaning
> Blockquote with attributionDirect quote from Avoca's source materials (engineering-onboarding doc, AGENTS.md files, Slack messages). Their words.
::: tip From the docsClose restatement of Avoca's source content in plain prose. Their thinking, paraphrased.
::: info Unsolicited OpinionOnly content Sandy himself has framed in conversation. Empty until Sandy says something.
::: details Q & AA specific question + a derived answer. The answer's source is named — Avoca's docs, code observation, or Sandy's reasoning.
Mermaid diagramsWorking mental model of how things fit together. Not authoritative; will be corrected as understanding develops.
::: warningOpen questions and unverified assumptions.

Source-of-truth material lives in engineering-onboarding.md (captured 2026-04-30 from app.avoca.ai/admin/engineering-onboarding). This doc references and synthesizes that material — it doesn't replace it.

On attribution discipline: an earlier draft of this doc labeled paraphrased source content as "Unsolicited Opinion." That was wrong — Sandy hasn't framed most of this yet. The convention now is strict: source content is attributed to Avoca, paraphrases say so, and the "Unsolicited Opinion" label is reserved for content Sandy himself originates.


What is Avoca

Avoca is an AI-powered voice assistant platform for the home services industry. We help HVAC, plumbing, electrical, and other service companies handle phone calls, book appointments, and manage customer interactions using AI.

— Avoca engineering-onboarding, Section 1

From the docs

Avoca sits between callers (end customers) and service businesses (Avoca's actual paying customers — HVAC shops, plumbers, auto-repair shops, etc.). When a customer calls a business that uses Avoca, an AI voice assistant answers, handles the conversation, and mutates the business's CRM (booking, rescheduling, lookup). This is the framing in Section 1 of engineering-onboarding.md, restated.

Q: who runs the actual voice infrastructure?

A (from Avoca's docs): Vapi — external service. Section 2 of engineering-onboarding.md lists VAPI under "Key integrations" as "Voice AI infrastructure." Vapi runs the assistant runtime (audio, LLM, TTS); Avoca's backend exposes the tools the assistant calls (booking, lookups, etc.).

Q: what's the relationship between Avoca and the CRMs?

A (from Avoca's docs): Avoca integrates into the business's existing CRM via API. Section 18 of engineering-onboarding.md lists ServiceTitan as the primary CRM, with 11+ others supported (Jobber, Salesforce, FieldRoutes, AutoOps, IAA, etc.). Each has its own workflow module.

Unsolicited Opinion

Empty — to be filled when Sandy frames the relationship between Avoca, callers, businesses, and CRMs in his own words.


Highest-level system context

Reproduced from Avoca's engineering-onboarding Section 2 "System Architecture" React Flow diagram. Verified against captured screenshot (2026-05-01). Color semantics match the original legend: amber = live call flow (dashed), blue = internal, gray = external, green = async save, purple = LLM calls.

From the docs (Section 2 caption)

The amber path is the inbound call flow — the core of Avoca. A customer calls a Twilio phone number, which forwards to VAPI (the voice AI platform). VAPI runs the conversation and invokes tool endpoints in apps/web for business operations (booking, customer lookup, etc.). When the call ends, VAPI sends an end-of-call-report webhook back to web, which triggers post-call processing — saving the call record to Supabase and kicking off async Inngest jobs (call analysis, email alerts, CRM sync).

From the docs

Section 2 of engineering-onboarding.md (verbatim): "apps/web is the brain — it owns all business logic, APIs, webhooks, and external integrations. apps/dashboard is primarily a thin UI that proxies most requests to web, though some features (fees, transfer destinations) have direct DB access. apps/avo is the AI chat runtime that calls web for business operations via a service token, but also has direct DB access for some queries."

Other facts stated in the same section:

  • "Background jobs run on Inngest."
  • "Deploys go to Vercel."
  • "The database is Supabase (Postgres)."

Unsolicited Opinion

Two readings developed in research-mode dialog (2026-05-01 session):

  • Inngest is the workflow engine for everything that doesn't fit a request/response cycle: post-call analysis, CRM sync, scheduled jobs, fan-out work. 158 function files in apps/web/lib/inngest — verified by directory listing. That number signals a lot of distributed orchestration; at this volume, you start to need an orchestration-of-orchestration layer.
  • Vapi is external. Avoca doesn't run voice AI — they run the tools the voice AI calls. This is a sharp boundary worth remembering when reasoning about latency, observability, and where bugs live: anything inside the audio loop is Vapi's; anything reachable from a tool call is Avoca's.

Verified observations (from code/repo, not docs)

  • No vercel.json in next/ — confirms default Vercel serverless deployment with no custom function config.
  • No Docker, k8s, or persistent-server config anywhere — confirms fully serverless.
  • Workflow tooling deps in apps/web/package.json: only Inngest (no Temporal, Restate, Cadence, Trigger.dev).

Open questions

  • How does the assistant know which CRM tools it has? (Drives Plan E — learn-assistant-config-and-deployment.)
  • The Twilio → Vapi handoff — is there any Avoca-side webhook on the inbound side, or does Vapi own the call entirely until tools fire?

Resolved (2026-05-01) — Common-webhook playbook scope

The "common-webhook playbook" is now captured at integration-playbook.md. It's specifically about post-call webhook architecture — a single shared Vapi end-of-call webhook (end-of-call-report.ts) with config-driven Triager + Factory dispatch on vertical and crm. It does NOT replace in-call Vapi tool dispatch, but it does include rules for live tools: handlers under lib/tools/<vertical>/, API routes under pages/api/vapi/tools/<vertical>/, CRM config from Supabase (no env-var creds), shared CRM client per CRM. The boundary between in-call tool dispatch and post-call workflow is therefore: tools run during the call (request/response shape), workflows run after end-of-call-report fires (async, Inngest-orchestrated, vertical-specific outcome determination + per-CRM workflow stages).


Per-product-area diagrams

Reproduced from Avoca's engineering-onboarding React Flow diagrams (Sections 9, 10, 11, 12, 13, 14, 15, 16). Diagrams verified against screenshots provided 2026-05-01. Captions are quoted from each section's intro paragraph.

Index:


Responder — Inbound AI Calls (Section 9)

The core product. AI agents answer inbound customer calls, book appointments, handle emergencies, and transfer to humans when needed.

Unsolicited Opinion

Empty — to be filled when Sandy drills into the responder flow.

From the playbook

The Responder Webhook in this diagram is what the integration-playbook.md calls the "single shared Vapi end-of-call webhook URL — all verticals, all CRMs, all teams." The post-call Triager + Factory pattern (runWorkflowWorkflowRunTriagerFactory → per-vertical Triager → per-CRM workflow stage) lives behind it. Routing is determined by the team's vertical and crm config, not by URL.

Open questions

  • The Bookability Check sits between Data Extraction and ServiceTitan booking. For AutoOps reschedule, is there an analogous gate, or does AutoOps cancel/reschedule bypass it because they're in-call mutations?
  • Post-Call Jobs is the Inngest fan-out point — what's the actual function set triggered here? (Drives Plan D.)
  • The * in /api/responder/*/webhook — playbook says "single shared URL," so the wildcard probably routes everyone to the same handler, with vertical + crm config doing the dispatch inside. Worth confirming with a code read.

Outbound Campaigns (Section 10)

Workflow-driven outbound calling and texting. Target filtered audiences from CRM data, execute calls/texts, track results.

From the docs (campaign types)

Documented campaign types: Maintenance, Reschedule, Estimate Follow-Up, Speed-to-Lead, Dropped Responder, Unsold Estimates, Expiring Memberships, Survey.

Unsolicited Opinion

Empty — to be filled.

Open questions

  • "Reschedule" is a documented outbound campaign type. Is outbound reschedule (Avoca calls customer to reschedule) a different workflow from the inbound reschedule Plan F is building (customer calls in, agent reschedules during conversation)? Worth confirming the inbound/outbound distinction is structural and not just two paths to the same handler.
  • The Campaign Driver is an Inngest orchestrator — likely one of the bigger function sets in the 158-file count.

Texting & Messaging (Section 11)

Multi-channel text conversations — SMS, Google Local Service Ads, and Yelp messaging. AI-powered responses with TCPA compliance.

Unsolicited Opinion

Empty — to be filled.

Open questions

  • TCPA compliance is enforced at the Opt-Out Check step before any reply queues. Where does opt-out state live — text_conversations rows, or a separate opt_outs table?
  • Yelp's connection mode says "webhook" but Google LSA is "poll/webhook" — Google likely supports both, with poll as fallback. Implementation detail worth knowing if anything breaks.

Dispatching & Capacity (Section 12)

Automated technician dispatching — assigns jobs based on skills, availability, drive time, and performance.

Unsolicited Opinion

Empty — to be filled.

Open questions

  • ServiceTitan-only flow as drawn. Does AutoOps have an analogous dispatcher, or is auto-dispatch a ServiceTitan-only feature so far?
  • The Scoring Engine considers Drive time, callback, perf — what's "callback"? Phone-callback rate, or callback within a job? Naming is ambiguous.

Coach & Oversight (Section 13)

AI-powered call quality analysis. Every call is analyzed by an LLM for intent, fulfillment, and scored against custom rubrics.

Unsolicited Opinion

Empty — to be filled.

Open questions

  • Every call ends up here. Volume is high. The LLM Analysis step likely dominates LLM spend at Avoca scale. Worth checking sampling/throttling.
  • Rubrics are per-team (loaded from oversight_tags). Means each team's coach behavior is configurable — UI for that lives somewhere in the dashboard.

Avo — AI Chat Assistant (Section 14)

Text-based AI assistant in the dashboard and Slack. Answers questions about team data, calls, revenue, and KPIs.

From the docs (architecture boundary)

  • apps/avo — AI orchestration only. Owns prompts and model routing. Has some direct DB reads, but business mutations go through web.
  • apps/web — Business boundary. All data queries and mutations go through web.
  • Service token auth — Avo calls web via a gateway with a shared service token (AVO_SERVICE_TOKEN), not user sessions.

Unsolicited Opinion

Empty — to be filled.

Open questions

  • The AVO_SERVICE_TOKEN is a shared secret between apps/web and apps/avo. Per AGENTS.md, must be set locally to match the value used by the avo process. Standard service-to-service pattern.
  • Avo's "some direct DB reads" exception is interesting — same pattern as dashboard's exceptions. Both apps have read paths that bypass the canonical API. The migration mentioned in onboarding doesn't appear to cover Avo's reads.

Simple Scheduler (Section 15)

Embeddable booking widget for company websites. Customers can self-service book appointments with real-time ServiceTitan availability.

From the docs (how it works)

  1. Customer visits company website with embedded widget
  2. Enters zip → service area validated
  3. Selects service → availability queried from ServiceTitan
  4. Picks time, enters details → job created in CRM

Unsolicited Opinion

Empty — to be filled.

Open questions

  • ServiceTitan-specific flow as drawn. AutoOps has its own booking flow via the AutoOps API (POST /booking-flow/{clientId}/book) — does Simple Scheduler support AutoOps too, or is it ST-only?
  • simple_scheduler_configs is a separate table from assistant_configs. Two different config surfaces — worth understanding which one drives reschedule UX (probably assistant_configs, since reschedule is in-call).

Leads & Speed-to-Lead (Section 16)

Ingest leads from multiple sources, qualify them, and track speed-to-lead (time from arrival to first contact).

From the docs (lead sources)

Google LSA, Yelp, Web forms, ST Bookings, CSV Import (manual upload), Dropped Calls (unbooked Responder calls).

Unsolicited Opinion

Empty — to be filled.

Open questions

  • "Dropped Calls" is itself a lead source — Responder calls that didn't result in a booking become Speed-to-Lead leads. Closes the loop between inbound and outbound. Worth understanding the trigger criteria.
  • Speed-to-Lead "time tracking" — is the metric arrival-to-first-contact regardless of channel, or per-channel? Likely the former for the user-facing metric.

App boundaries (TODO — drill in next)

apps/web is the canonical server authority for business logic. Dashboard proxies most requests to web (some features have direct DB access as exceptions). Avo calls web for business tool execution via a service token, but also queries the DB directly for some read operations. — Section 3, engineering-onboarding.md

From the docs

The pattern is: canonical-API + thin-clients, with two documented exceptions where dashboard and avo bypass the API and read the DB directly. The Dashboard API-First Migration (Section 5 of engineering-onboarding.md) is the active effort to close those gaps.

Unsolicited Opinion

Empty — to be filled when Sandy works through this section.

To fill: detailed mapping of which routes live where, how service tokens work, how the verifyTeamAccess auth gate is invoked, the migration's current state.


Data layer (TODO)

To fill: Supabase schema highlights relevant to AutoOps work, the encryption-at-rest pattern (WEBHOOK_API_KEY_ENCRYPTION_KEY), the autoops mirror tables, RLS posture, connection pooling.


Workflow layer (TODO)

To fill: Inngest function shapes (cron vs event), the step.run durability primitive, post-call workflow orchestration, the autoops mirror sync, common patterns Sandy needs to recognize.

Canonical pattern

Post-call workflow architecture is documented in integration-playbook.md: single shared Vapi end-of-call webhook → WorkflowRunTriagerFactory → per-vertical Triager (extends WorkflowRunTriager) → per-CRM workflow stage. New verticals plug in here. Home Service is the default fallback and is never modified.


Tool dispatch flow (TODO)

To fill: how a Vapi-fired tool call traverses pages/api/vapi/tools/dispatch.ts, the per-tool handler shape, the magic-string-switch antipattern, where the AutoOps tools fit, where the per-team handlers sit (Peterman, etc.).

Canonical pattern (live tools section of playbook)

Per integration-playbook.md, live tool calls follow:

  • Tool handler at lib/tools/<vertical-name>/<toolName>Tool.ts
  • API route at pages/api/vapi/tools/<vertical-name>/<toolName>.ts
  • Tests at pages/api/vapi/tools/<vertical-name>/__tests__/
  • CRM config from Supabase (no env-var creds)
  • Shared CRM client from lib/<crm-name>/
  • Accept teamId, lookup credentials dynamically — no isTest flag

Note: PR #9450 (AutoOps cancel) put handlers at lib/vapi/tools/handlers/autoOpsCancelBooking.ts instead of lib/tools/<vertical-name>/. Otherwise compliant. Reschedule decision (mirror Kareem's location vs follow playbook strictly) is a Plan F ADR question.


External integrations (TODO)

To fill: per-CRM integration shapes — how AutoOps differs from ServiceTitan differs from IAA. The non-st-workflow/ framework. The four-seasons/ per-CRM directory pattern. ARS, ServPro, etc.

Canonical pattern (CRM integration)

Per integration-playbook.md, each CRM integration consists of:

  • lib/<crm-name>/<crm>.ts — generic auth + CRUD client (vertical-agnostic, reusable across verticals)
  • lib/supabase/<crm>.ts — config fetcher (get<Crm>Config(teamId))
  • <crm>_config Supabase table per CRM with encrypted credentials and updated_at trigger
  • Workflow stages at lib/workflow/stages/<workflow-type>-<crm>/

Stage anatomy: fetch CRM config → run LLM extraction (vertical-specific prompt + schema) → resolve address → conditionally post to CRM → update call record → send email notification.


Open questions running list

  • (Carried from System Context section above) — assistant_configs lookup, common-webhook playbook scope, Twilio inbound webhook role, tool-vs-workflow boundary for new features.
  • Add as questions surface during drill-down.

Source materials

  • Engineering onboarding (captured)
  • Integration playbook (captured) — canonical pattern for adding new verticals + CRMs (post-call Triager + Factory pattern, live tools layout, six design principles)
  • next/apps/web/AGENTS.md — slice-specific architectural guidance (Avo, Asana webhooks, Vercel project layout, local dev setup)
  • Slack: @here Common-Webhook Playbook announcement (cc'd Christian Scarlett, Omkar Ingale, Bharat Kilaru)
  • Notion: New Vertical CRM Integration Playbook — pending access from Kareem