Architecture
This page describes the production stack, the request path for a chat message, the database model and the trust boundaries. It is a technical deep dive — read the Overview first if you want the product-level picture, or AI concepts from scratch for the machine-learning vocabulary the architecture implements.
The concepts this architecture implements
Section titled “The concepts this architecture implements”Everything below wires together the ideas from the AI primer:
- The request path is one LLM call through a proxy. The client holds the key (BYOK), wraps it in a handoff envelope, and the server proxies
streamTextto the provider — so the model’s token loop runs at the edge while your key never touches our storage. - Local-first answers the context problem. Because the model only sees what’s put in its window, the platform must re-supply memory, retrieved docs and history every request — so it keeps heavy data (messages, code, diffs) locally (IndexedDB/bridge SQLite) and only a light index server-side. Fast offline reads, privacy by default.
- Structured output is a first-class contract. Plans and artifacts are recovered from model text as validated schema objects (
Output.object/ Zod + tolerant scanning), not free prose, which lets other surfaces (studio, artifacts) consume them programmatically. - Agents / tools / MCP extend a single model into a system that can act on your repos, devices and services.
The stack table and request path below are how those ideas show up in actual technology.
| Layer | Technology | Notes |
|---|---|---|
| Web framework | Astro 6 | Static marketing pages + server routes; islands for interactive components |
| Client routing | TanStack Router (file-based) | The dashboard shell (/space) uses a hand-built route tree in DashboardApp.tsx; marketing pages use Astro routes |
| UI | React 18+ (function components only), Tailwind CSS v3, shadcn/ui primitives, Radix | No class components anywhere |
| Runtime | Cloudflare Workers | Stateless per-isolate, no fs, no Node-only APIs |
| Database | Postgres (Supabase) via Drizzle ORM | Migrations generated with drizzle-kit generate, never hand-written |
| AI | AI SDK v4 | streamText + Output.object({ schema }) for structured output; generateText for non-streaming |
| Media storage | R2 (Cloudflare) | Content assets and uploads |
| State | IndexedDB + localStorage (client) with an encrypted vault | Local-first hybrid; light Postgres index for cross-device sync |
| Auth | Magic-link + OTP codes | src/lib/auth.ts / auth-server.ts |
| Payments | LemonSqueezy | Webhook flips users.tier between free and paid |
Request path for a chat message
Section titled “Request path for a chat message”- Client reads the active provider/model and the stored API key from the encrypted local vault (
credentials.ts). - The client builds a single-use handoff envelope: the plaintext key is wrapped for the server’s public E2EE key with a short expiry (
buildVaultHandoffincredentials.ts→kdf.ts). - The envelope travels to
POST /api/chat(src/pages/api/chat.ts). The route first callsrequireResolvedAuth(cookies, request)— the single auth gate for every API route. - The server unwraps the envelope, resolves the provider model (
ai-providers.ts), and streams the response back withstreamText(AI SDK v4). Structured outputs useOutput.object({ schema }). - Each message is persisted server-side through
chat-persistence.ts(Postgres) and client-side throughlocal-chat-store.ts(IndexedDB). Usage records (recordUsage) are written tousage_records. - The client renders the stream incrementally. Artifacts and plans are extracted from the assistant text on the client (
extractPlanFromContentinchat-helpers.ts).
Why local-first with a light server index?
Section titled “Why local-first with a light server index?”Messages, code and diffs are heavy. Storing them only in the browser (IndexedDB) and in the paired bridge (SQLite) keeps the app fast and private. A ~200 B row per conversation in Postgres (conversations) is enough to sync titles and timestamps across devices.
Database model (Postgres)
Section titled “Database model (Postgres)”The Drizzle schema lives in src/lib/db/schema.ts. Core tables:
| Table | Purpose |
|---|---|
users |
Identity: email, name, tier (free/paid), preferences (JSONB), verification flag |
sessions |
Server sessions (token + expiry) for authenticated API calls |
verification_codes |
Email OTP codes with expiry and attempt count |
conversations |
Chat sessions: title, provider/model, fork tree (parentId, rootId, childCount), workspace ref (JSONB), thinking level, token counter, pinned/tags/status |
messages |
Per-conversation messages: role enum, JSONB content, token count, model, metadata, tsvector search column |
folders |
Conversation folders with color/icon/order |
agents |
Agent catalog: role, description, system prompt, model/provider, tools (JSONB), category, pricing tier |
subscriptions |
LemonSqueezy subscription links (agent subscriptions) |
workflows, workflow_nodes, workflow_edges, workflow_runs |
Pipeline studio persistence |
content_assets |
Generated media (image/video/audio/3D) |
vault_entries, user_vault_config |
Optional server-side vault sync (only with explicit per-provider opt-in) |
usage_records |
Token/cost metrics per request: prompt/completion/cached tokens, reasoning tokens, duration, mode, cost USD, tier |
devices, device_tokens, pairing_codes, device_action_log |
Bridge pairing, device tokens, pairing codes and the consent audit log |
chunk_hashes |
Content-addressable dedup for uploads |
task_outbox |
Outbox pattern for async jobs |
Trust boundaries
Section titled “Trust boundaries”- Your keys never leave your device except inside a short-lived E2EE handoff envelope, and only for the duration of the request you initiate. This is the BYOK promise — see Security & BYOK.
- The bridge daemon (a separate Rust repo) connects outbound-only via WebSocket to the platform; no inbound ports are opened on your machine. Every action requires consent scoped to a capability (
desktop.shell.execute,desktop.fs.read, …). - API routes are gated by
requireResolvedAuth; CORS/rate limiting/redaction live insecurity.ts; sensitive logs go through the singlelog-redaction.tssink.
Client-side storage keys
Section titled “Client-side storage keys”All client persistence keys carry the synthhires:* prefix (see src/lib/nickname.ts for the canonical helpers). Notable keys: synthhires-credentials (encrypted vault), synthhires-tokens (service tokens), synthhires:activeConvId, synthhires:activeProvider, synthhires:enabledModels, synthhires:systemInstruction, synthhires:globalParams, synthhires:sidebar:width.
Conventions worth knowing
Section titled “Conventions worth knowing”- IDs are always
crypto.randomUUID()— never timestamp-based strings (collision-prone with concurrent forks). - Cross-component events use colon syntax:
synthhires:conv:focus,synthhires:nickname:update,synthhires:sidebar:set. - Every transition carries
motion-reduce:transition-none motion-reduce:duration-0;transition-allis banned.