Architecture
A short tour of what happens between POST /runs and a finished report.
High-level
[Clerk: customer auth] ↓[Customer browser] → [Next.js: app.syntheticusers.io] ↓ API key[API Worker: api.syntheticusers.io] ├→ [D1: customers, runs, sessions, personas, credentials, webhooks, usage] ├→ [KV: API key lookup (hot path)] ├→ [Stripe: usage events] └→ [SessionDO per session] ⇄ [Container DO: agent-browser + Chromium] ↓ [R2: transcripts, screenshots, reports] [DO SQLite: live per-session reactions]
[CF Email Routing: inbox.syntheticusers.io → Email Worker] ↓ [InboxDO per session]Per-session lifecycle
POST /runs→ API Worker validates, writes aRunrow in D1, returns{ run_id, status: "queued" }in under 500ms.- RunDO spawns one SessionDO per persona in parallel (1-10).
- Each SessionDO instantiates an AgentBrowserContainer DO subclass
keyed by
<session_id>#<containerGen>. Container boots a self-hosted Chromium + theagent-browserRust daemon. - The Pi orchestrator loop runs inside the SessionDO. Each turn:
- Snapshot the page; pass ref handles to the LLM.
- LLM picks an action (click, type, scroll, navigate,
react(),finish()). - SessionDO writes the message + any reactions into DO SQLite.
- If the Container crashes (CDP socket drop, OOM), SessionDO increments
containerGenand reconnects. Reactions are preserved; messages from the crashed gen are dropped. The agent getspriorReactionsin the prompt and re-explores. - When the turn budget runs out, the agent is required to call
finish(), and the report synthesis step runs. - SessionDO writes
transcript.json.gz,report.json,report.md,report.html, andscreenshots/*.pngto R2 undercustomers/<cust>/runs/<run>/sessions/<sess>/. - The last session to finish triggers AggregateDO; aggregate-report files
land at
customers/<cust>/runs/<run>/. - UsageEvent rows are written and posted to Stripe Meter Events
(idempotent on
usage_event.id). - Webhooks fire (
run.completed).
Why Cloudflare Containers
Cloudflare Containers give each session its own isolated browser that starts in milliseconds, with no per-tool-call vendor lock-in and a commodity Chromium underneath. The cost of a run is dominated by LLM tokens, not the browser infrastructure.
Why this shape (vs. a stateless worker)
A run is a multi-minute, stateful conversation between an LLM and a browser. Workers plus Durable Objects give us both halves — the conversation (DO SQLite holds the messages and reactions) and the browser (the Container) — in the same edge region, billed by CPU time per request rather than per tool call.
Security model
- SSRF: Only HTTPS targets. Private IPv4/IPv6 /
.internal/.localrejected at the API layer. Container egress also constrained. - Auth: API key on every request (see Authentication for rate limits). The dashboard signs in through Clerk.
- Credentials: Envelope encryption (a master key wraps a per-customer key). The plaintext password is only decrypted inside the session, at the moment the agent needs to log in.
- PII in transcripts: emails and phone numbers are masked before anything is written to storage.
- Retention: artifacts are kept for 30 days by default.