Storage
Two storage planes plus a sync layer plus a per-agent runtime cache. Postgres (Supabase) holds structural truth in per-entity tables — houses, agents, members, threads, environments, configs, secrets, invites, api_keys. Durable Streams (Electric SQL) hold append-only content — one stream per thread at arbe-thread-{id}, immutable historical reads, CDN-cacheable. Electric SQL syncs Postgres → browser as TanStack DB collections. Cloudflare DO SQLite is the per-agent compute runtime — unrelated to durable streams despite the shared word.
structural data: browser ◄──► Electric ◄── Postgres ◄── per-entity REST routesthread content: browser ◄──► /api/threads/:id/stream (proxy) ◄──► Durable Streamsmembership/RLS: is_house_member checks gate every house-scoped tableoptimistic write: collection applies → POST/PATCH/DELETE → txid → Electric reconcilesThe browser never queries Postgres directly — it subscribes to shapes proxied through /api/shapes/{table}. The proxy injects table name, columns, WHERE clause, and Electric source credentials; the client only sends protocol params (offset, cursor). This keeps the Electric secret server-side and lets the proxy enforce per-agent scoping. Visibility starts at house scope: per-entity shapes filter on house_id = $house AND deleted_at IS NULL. Membership changes produce a different WHERE on the next request, triggering a full re-sync (shapes are immutable per subscription). /api/shapes/members scopes to rows the caller can see (own membership + peers in shared houses).
Thread entries flow browser ↔ Durable Streams, proxied through /api/threads/:id/stream. The proxy checks membership and hides the stream secret from the browser. Reads support long-poll tailing for real-time updates. POST /api/threads/:id/entries writes user/system entries and fires dispatch (mention fan-out for in-process bots, or remote pi on env-bound threads).
Optimistic writes use TanStack DB’s mutation support: client applies locally, calls the matching per-entity write route (POST/PATCH/DELETE under /api/{houses,agents,environments,configs,threads} — see apps/www/src/lib/collections/write.ts), awaits the returned txid for Electric reconciliation. Rollback on failure. Stream writes POST to /api/threads/:id/entries and confirm through the tail — duplicates from optimistic inserts are skipped by ID.
The CLI has no local database. Threads, messages, structural data all go through the HTTP API — Postgres is the sole source of truth, with no local mirror, cache, or offline fallback. Files under .arbe/ (pi session JSONL, task JSON, logs) are workspace state, not product data. The contract: Postgres threads owns thread metadata (kind, status, usage, config); the thread’s durable stream owns the full transcript + lifecycle history.
New synced tables follow the same policy: a house_id column (FK or denormalised via trigger) so the shape WHERE stays a literal equality (house_id = $1 AND deleted_at IS NULL).
Code: packages/supabase/migrations/, apps/www/src/routes/api/shapes/, apps/www/src/lib/collections/write.ts.
See sync, system/durable-streams, system/permissions.