0001 — Decouple web app from agents
Context
The Next.js web app moved to Vercel in April 2026 but the agent runtime stayed entangled with it. Clara's Lambda bundles 16 modules from src/lib/ via esbuild. When those files change on Vercel's deploy pipeline but Lambda isn't redeployed, the two drift — causing production incidents on April 13 (Clara replied to wrong address) and April 15 (4 PRs merged, Lambda ran day-old code).
We also had:
- Two processes reading the same SQS queue (EC2 consumer + Lambda) based on a toggle that was misaligned.
- An Outlook webhook fire-and-forget killed by Vercel's freeze, causing days-old Zillow leads to be re-processed.
- An embarrassing email sent to a prospect reading "this is a duplicate of the original lead email — no further action needed" because the
inbox_emailsend path lacks the dedup lock the SES path has. - Lambda v48 shipping a pdf-parse bundling bug that crashed on every invocation with no pre-deploy smoke test.
Root cause: no clean boundary between what runs on Vercel and what runs in AWS. They share runtime code, and the drift surface is wherever they overlap.
Decision
Vercel runs the web app.
- Dashboard UI (Next.js pages for property managers)
- PM-facing CRUD APIs (list prospects, work orders, tenants; approve/reject; settings)
- Auth (Better Auth, magic link, OAuth)
- Billing (Stripe checkout, portal, webhook)
- All inbound webhooks from third parties (Twilio SMS, Twilio voice, ElevenLabs voice + tools, SendGrid, SES, Outlook/Graph, Telegram)
- Webhook bodies are thin: validate signature → publish to SQS → return 200. No business logic, no agent code. Voice tools (sub-2s RPC) read/write DDB directly but do not invoke the agent loop.
AWS runs the agents.
- Lambda
agents/clara— inbound agent loop (consume SQS, run Claude, save conversation, dispatch outbound SMS/email) - Lambda
agents/tour-reminder— EventBridge-triggered, sends reminder SMS - Lambda
agents/outlook-subscription-renewer— renews Graph webhook subscriptions - Lambda
agents/listings-sync— scrapes AppFolio, updates unit availability - Lambda
agents/application-link— EventBridge one-shot post-tour-confirmation - EC2
agents/browser— headless Chromium for AppFolio automation
Contract between Vercel and AWS: DynamoDB tables + SQS queues. That's it. Zero shared TypeScript files (see ADR-0002).
Consequences
Easier
- Drift surface collapses to zero because Vercel's
src/and AWS'sagents/don't share runtime code. - Each runtime has its own deploy pipeline, its own failure domain, its own scaling story.
- Clara can be rolled back without rolling back the web app and vice versa.
- Onboarding to one side doesn't require understanding the other.
Harder
- Schema changes to shared DDB tables require coordinated thinking (see ADR-0002).
- Duplicated DDB client code and types on both sides. We accept this cost.
- Webhook receivers on Vercel need to be strictly thin. Code review must reject PRs that put business logic in webhook routes under
src/app/api/. - Voice tools still run on Vercel because ElevenLabs needs <2s response. They must not import from
agents/.
Follow-up work
- Move
src/lib/agent/**,src/lib/email/process-*.ts,src/lib/messaging/**,src/lib/email/inbox-client.ts, and related agent code intoagents/clara/lib/. - Delete these files from
src/once Clara is fully running fromagents/clara/. - Add a CI lint rule preventing
src/from importingagents/and vice versa.
Alternatives considered
Shared package (packages/shared) via npm workspaces. Originally proposed in Trello #762's initial description. Rejected April 14 because it "tightly couples web app to workers, defeating independent deploys." A shared package doesn't solve the drift problem — it relocates it.
Shared types package only (packages/types, Zod schemas). Seriously considered. Rejected for maximum decoupling; even shared types create a deploy-coordination burden on schema changes.
Thin Lambda that posts back to Vercel for processing. Rejected because it violates "Vercel is web-only." Agent runtime belongs in AWS where we have the right tools (SQS, EventBridge, long Lambda durations).
Keep current coupled state and just add drift detection. Rejected because drift detection is a band-aid. The Apr 13 and Apr 15 incidents happened with drift warnings in CI — they just weren't blocking. Fixing the structural problem is better than fortifying the warning.