Skip to content
View as .md

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 hold append-only content — one stream per thread at arbe-thread-{id}, immutable historical reads, CDN-cacheable. The www API serves Postgres to the browser, which caches it as TanStack DB query collections. Cloudflare DO SQLite is the per-agent compute runtime — unrelated to durable streams despite the shared word.

structural data: browser ◄──► /api/* routes ◄── Postgres
thread content: browser ◄──► /api/threads/:id/stream (proxy) ◄──► Durable Streams
membership/RLS: is_house_member checks gate every house-scoped table
optimistic write: collection applies → POST/PATCH/DELETE → refetch reconciles

The browser never queries Postgres directly — every structural read is a www API route that resolves the caller, checks membership, and returns rows gated by RLS. Collections fetch those routes and parse the rows through the core schemas; which ones poll and which refetch only on focus is in sync. Membership changes are picked up by the membership collection, and a 403/404 from a house-scoped fetch evicts that collection rather than leaving stale rows mounted.

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 wakes the thread’s director; the backstage decides the speaker and runs the turn.

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), then refetches the collection so the server row replaces the optimistic one. 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 house-scoped list route filters on a literal equality (house_id = $1 AND deleted_at IS NULL).

Code: packages/supabase/migrations/, apps/www/src/lib/collections/, apps/www/src/routes/api/.
See sync, system/durable-streams, system/permissions.