# 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](sync.md). 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/`.<br>
See [sync](sync.md), [system/durable-streams](../chat/durable-streams.md), [system/permissions](../access/permissions.md).
