# 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 routes
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 → txid → Electric reconciles
```

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