Skip to content

Code Path: Live AutoOps vs Mock AutoOps

Full technical walkthrough of how a single call routes through avoca-next — twice. First showing the live AutoOps path (real CRM, production-shape), then the mock AutoOps path (our WireMock substrate). Same call shape, same agent, same tools. The two paths diverge at exactly two points: getAutoOpsConfig (short-circuit vs Supabase + decrypt) and the URL constant inside the AutoOps client.

This page exists so questions like "where does decryption happen?" or "is Supabase queried in the mock path?" can be answered by tracing the diagram, not by re-asking.

Reading guide

  • Two stages per path: in-call (tool invocations during the live conversation) and post-call (Inngest workflow that runs after Vapi hangs up).
  • File paths in callouts use next/... since that's how the canonical clone is mounted in the toolkit.
  • Divergence points are highlighted in red boxes / notes.
  • Both diagrams assume the same starting condition: a Hamming-driven test call to a test phone bound to one of the EAS teams.

In-Call Code Path — LIVE AutoOps

Configuration:

  • AUTOOPS_BASE_URL = 'https://api.autoops.com/v1' (production default in apps/web/lib/autoops/autoops.ts:20)
  • MOCK_AUTOOPS_CONFIGS = [] (no short-circuit; production has no test-team hardcodes)
  • WEBHOOK_API_KEY_ENCRYPTION_KEY env var IS set (so decryption succeeds)
  • Team's autoops_team_configs + autoops_credentials rows exist in Supabase

Architecture (component topology):

Divergence components highlighted: red AUTOOPS_BASE_URL (the URL constant that gets swapped on test branches) and yellow getAutoOpsConfig (the function where the short-circuit branch is inserted on test branches).

Time-ordered sequence (same flow, step by step):

What ran on the real backend: the GET hit api.autoops.com. The shop's actual schedule was read. Real customer records, real services, real availability.

What touched Supabase: one row read from autoops_team_configs JOINed with autoops_credentials.

What got decrypted: the team's apiKey (one AES-256-GCM operation per getAutoOpsConfig call, per tool invocation).


In-Call Code Path — MOCK AutoOps

Configuration (our test worktree test/mock-autoops-southpark):

  • AUTOOPS_BASE_URL = 'https://api-mocker.dft.dawn' (hardcoded override on this branch)
  • MOCK_AUTOOPS_CONFIGS = [...] includes teams 2970 / 2977 / 2976 with hardcoded plaintext apiKeys + real AutoOps clientIds (so shop-routing.json works)
  • WEBHOOK_API_KEY_ENCRYPTION_KEY env var is NOT set locally (and that's fine because we never need to decrypt)
  • Supabase rows may or may not exist for the test teams (irrelevant either way)

Architecture (component topology):

Differences from the live diagram, at a glance:

  • Green components are the mock-specific divergences (MOCK_AUTOOPS_CONFIGS array, AUTOOPS_BASE_URL pointing at WireMock).
  • Faded grey, dashed components (Supabase tables, WEBHOOK_API_KEY_ENCRYPTION_KEY) are present in the codebase but never reached in this path. The short-circuit returns BEFORE they would be touched.
  • The HTTP destination is WireMock instead of real AutoOps.
  • The decryptApiKey node disappears entirely (no encrypted credentials to unwrap).

Time-ordered sequence:

What ran on the real backend: nothing. Zero traffic to api.autoops.com.

What touched Supabase: nothing. getAutoOpsConfig short-circuited before the query.

What got decrypted: nothing. decryptApiKey was never called. WEBHOOK_API_KEY_ENCRYPTION_KEY is irrelevant in this path.

What WireMock saw: one GET, with the real EAS clientId in the path, an Authorization header containing the literal string "Bearer mock-key", and an empty query/body. WireMock matched the stub URL pattern and returned canned JSON.


In-Call Divergence Summary

StepLive AutoOps pathMock AutoOps path
1. dispatcheridenticalidentical
2. tool-call-mock lookupidentical (no mock configured)identical (no mock configured)
3. real handler entryidenticalidentical
4. getAutoOpsConfig(teamId)falls through to Supabasereturns hardcoded mock config (short-circuit)
5. Supabase queryruns (1 row)never runs
6. decryptApiKeyruns (requires WEBHOOK_API_KEY_ENCRYPTION_KEY)never runs
7. AutoOps client constructionidenticalidentical
8. HTTP request destinationhttps://api.autoops.com/v1https://api-mocker.dft.dawn
9. response handlingidenticalidentical

Two divergence points. Everything else is bit-for-bit the same code path.


Post-Call Code Path — LIVE AutoOps

After Vapi hangs up, the end-of-call webhook fires. Eventually that lands in an Inngest function which calls the booking stage. The booking stage repeats the same getAutoOpsConfigAutoOps client chain we just saw.

Architecture (component topology):

A real booking is created in EAS's AutoOps tenant. A real job_* ID comes back.

Time-ordered sequence:

A real booking is created in EAS's AutoOps tenant. A real job ID, customer ID, vehicle ID come back. If you don't want this to land in real prod data, cancel the job immediately afterward (per CLAUDE.md gotcha: "schedule a job, grab its id, cancel/reschedule that id").


Post-Call Code Path — MOCK AutoOps

Same Vapi end-of-call event. But RESPONDER_COMMON_WORKFLOW_URL is rewritten by Plan P's overlay to derive from NGROK_BASE_URL, so common-webhook traffic lands on the LOCAL Next.js process (not Avoca's hosted common-webhook). Local Inngest dev picks it up. Same workflow, same booking-autoops stage. But getAutoOpsConfig short-circuits, and the URL is hardcoded to WireMock.

Architecture (component topology):

Three transport-layer divergences from the live post-call diagram (in addition to the config short-circuit and URL constant):

  • ngrok tunnel replaces direct Vapi-to-prod routing
  • common-webhook route runs on local avoca-next instead of Avoca's hosted endpoint
  • Local Inngest dev picks up events instead of Inngest cloud

All three are enabled by the Plan P overlay (af78a5c89e in worktree-configs/avoca-next.json) that rewrites RESPONDER_COMMON_WORKFLOW_URL to derive from NGROK_BASE_URL.

Time-ordered sequence:

No real booking is created. The mock returns synthetic IDs (mock-job-VNDMK3DO etc.). Nothing reaches api.autoops.com. Nothing reaches EAS's real tenant.


Post-Call Divergence Summary

StepLive AutoOps pathMock AutoOps path
1. Vapi end-of-call webhook URLAvoca-hosted prod common-webhookLocal Next.js via ngrok tunnel (Plan P overlay rewrites)
2. Inngest event routingInngest cloudLocal Inngest dev
3. Workflow triageidenticalidentical
4. booking-autoops stage entryidenticalidentical
5. getAutoOpsConfigByClientIdfalls through to Supabasereturns hardcoded mock config (clientId-keyed short-circuit)
6. Supabase queryruns (1 row, by client_id)never runs
7. decryptApiKeyrunsnever runs
8. AutoOps client constructionidenticalidentical
9. book POST destinationhttps://api.autoops.com/v1https://api-mocker.dft.dawn
10. response handlingidenticalidentical

Zoomed-in views of the two divergence points

Divergence 1: getAutoOpsConfig short-circuit

Source: next/apps/web/lib/supabase/autoops.ts on the test/mock-autoops-southpark branch.

ts
const MOCK_AUTOOPS_CONFIGS: AutoOpsConfig[] = [
  {
    teamId: 2970,
    apiKey: 'mock-key',
    clientId: 'cl_e019c08b5b3a49b5b7a990e67ceb3e6f', // real AutoOps clientId
    enabled: true,
    clientName: 'TEST - EAS - South Park',
    clientKey: null,
    timeZone: 'America/Denver',
  },
  // ... Platte Canyon (2977), Chatfield (2976) ...
];

export async function getAutoOpsConfig(
  teamId: number,
  log?: Logger
): Promise<AutoOpsConfig | null> {
  const mock = MOCK_AUTOOPS_CONFIGS.find((c) => c.teamId === teamId);
  if (mock) return mock;  // ← short-circuit. Supabase + decrypt never run.
  
  // Production code path below — Supabase query, decrypt, etc.
  // ...
}

export async function getAutoOpsConfigByClientId(
  clientId: string,
  log?: Logger
): Promise<AutoOpsConfig | null> {
  const mock = MOCK_AUTOOPS_CONFIGS.find((c) => c.clientId === clientId);
  if (mock) return mock;  // ← same short-circuit, clientId-keyed
  
  // Production code path below.
  // ...
}

Both keyings exist because the in-call flow looks up by teamId (caller's team) and the post-call booking flow looks up by clientId (the chosen shop, which may differ from the caller's team in cross-shop bookings).

What lives in the production code on main: neither the MOCK_AUTOOPS_CONFIGS array nor the if (mock) return mock; lines. Just the Supabase + decrypt path. The short-circuit is added only on test branches.

Divergence 2: AUTOOPS_BASE_URL constant

Source: next/apps/web/lib/autoops/autoops.ts on the test/mock-autoops-southpark branch.

ts
const AUTOOPS_BASE_URL = 'https://api-mocker.dft.dawn'; // ← test branch only

// In production (main), this line reads:
// const AUTOOPS_BASE_URL = 'https://api.autoops.com/v1';

The constant is module-level and read by every method of the AutoOps class (5 references at the time of writing). One literal-string change on the test branch reroutes every AutoOps HTTP call from this process to WireMock.

Important property: this is a process-global setting, not per-call. Every team's AutoOps traffic from this process goes to the destination this constant points at. There is no current mechanism to route some teams to real AutoOps and other teams to WireMock from the same process. That kind of split would require a different architecture (e.g., per-call baseUrl override on the AutoOps constructor, with the decision driven by an isTest signal).


Why decryption is irrelevant in the mock path

Reading the divergence diagrams together: decryption only matters when getAutoOpsConfig falls through to the Supabase branch. On the mock path, the short-circuit returns BEFORE Supabase is ever queried, so the encrypted apiKey is never read, and decryptApiKey is never invoked.

This means our local dev setup does NOT need WEBHOOK_API_KEY_ENCRYPTION_KEY for any team in MOCK_AUTOOPS_CONFIGS. For any team NOT in that array, calls would attempt the Supabase + decrypt path and fail because the env var isn't set — getAutoOpsConfig would return null and the call would bail before any HTTP request fires.

Practical implication for the "live enterprise via mock" use case: to route a real EAS team's calls through WireMock, add that team's entry to MOCK_AUTOOPS_CONFIGS with Kareem's plaintext apiKey + the real clientId. The short-circuit then bypasses Supabase + decrypt for that team too, the URL constant routes the HTTP to WireMock, and you can run a batch against the real enterprise's identity without needing the encryption key. The team's actual Supabase row stays untouched.


File index (one-stop reference)

ConcernFile
URL constant + AutoOps classapps/web/lib/autoops/autoops.ts
Team config resolution + short-circuitapps/web/lib/supabase/autoops.ts
Decryption (real path)apps/web/lib/api-keys/encryption.ts
Tool dispatcherapps/web/pages/api/vapi/tools/dispatch.ts
Per-tool mock layer (separate from this)apps/web/lib/tools/test-call-mock.ts
Availability tool handlerapps/web/lib/vapi/tools/handlers/autoOpsGetAvailability.ts
Booking commit tool (in-call, no-op)apps/web/lib/vapi/tools/handlers/autoOpsConfirmAppointment.ts
Booking stage (post-call)apps/web/lib/workflow/stages/booking-autoops/run-booking-autoops.ts
Test-phone server URL (Plan P overlay)apps/web/lib/voice-assistants/test-phone/provision-test-phone.ts
WireMock instanceapps/api-mocker/
WireMock stub mappingsapps/api-mocker/mappings/autoops/