Appearance
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
::: infocallouts. Diagrams are Sandy's mental model — not Avoca's official architecture.
How to read this doc
| Convention | Meaning |
|---|---|
> Blockquote with attribution | Direct quote from Avoca's source materials (engineering-onboarding doc, AGENTS.md files, Slack messages). Their words. |
::: tip From the docs | Close restatement of Avoca's source content in plain prose. Their thinking, paraphrased. |
::: info Unsolicited Opinion | Only content Sandy himself has framed in conversation. Empty until Sandy says something. |
::: details Q & A | A specific question + a derived answer. The answer's source is named — Avoca's docs, code observation, or Sandy's reasoning. |
| Mermaid diagrams | Working mental model of how things fit together. Not authoritative; will be corrected as understanding develops. |
::: warning | Open 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.jsoninnext/— 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 (§9)
- Outbound Campaigns (§10)
- Texting & Messaging (§11)
- Dispatching & Capacity (§12)
- Coach & Oversight (§13)
- Avo — AI Chat Assistant (§14)
- Simple Scheduler (§15)
- Leads & Speed-to-Lead (§16)
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 (runWorkflow → WorkflowRunTriagerFactory → 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, withvertical+crmconfig 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_conversationsrows, or a separateopt_outstable? - 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_TOKENis a shared secret betweenapps/webandapps/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)
- Customer visits company website with embedded widget
- Enters zip → service area validated
- Selects service → availability queried from ServiceTitan
- 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_configsis a separate table fromassistant_configs. Two different config surfaces — worth understanding which one drives reschedule UX (probablyassistant_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 — noisTestflag
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>_configSupabase table per CRM with encrypted credentials andupdated_attrigger- 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:
@hereCommon-Webhook Playbook announcement (cc'd Christian Scarlett, Omkar Ingale, Bharat Kilaru) - Notion: New Vertical CRM Integration Playbook — pending access from Kareem