Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

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.

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 streamText to 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
  1. Client reads the active provider/model and the stored API key from the encrypted local vault (credentials.ts).
  2. The client builds a single-use handoff envelope: the plaintext key is wrapped for the server’s public E2EE key with a short expiry (buildVaultHandoff in credentials.tskdf.ts).
  3. The envelope travels to POST /api/chat (src/pages/api/chat.ts). The route first calls requireResolvedAuth(cookies, request) — the single auth gate for every API route.
  4. The server unwraps the envelope, resolves the provider model (ai-providers.ts), and streams the response back with streamText (AI SDK v4). Structured outputs use Output.object({ schema }).
  5. Each message is persisted server-side through chat-persistence.ts (Postgres) and client-side through local-chat-store.ts (IndexedDB). Usage records (recordUsage) are written to usage_records.
  6. The client renders the stream incrementally. Artifacts and plans are extracted from the assistant text on the client (extractPlanFromContent in chat-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.

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
  • 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 in security.ts; sensitive logs go through the single log-redaction.ts sink.

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.

  • 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-all is banned.