Skip to content

Client internals

Client internals

The client is the browser half of the runtime. This page is what happens inside it — the local replica, which reads it may answer itself, the commands a write travels on, and how several tabs share one copy.

The local replica

Each client keeps a policy-scoped PGlite database in IndexedDB, provisioned from the tenant\u2019s own migrations and then filled by a paged snapshot of every collection the subject may read. Storage is keyed by tenant and environment, so one browser signed into two workspaces never points both at the same rows. Collections compiled with sync: false — session, account, verification, and the internal bolt_* bookkeeping — are the deliberate exception: they never replicate, whatever the subject is otherwise allowed to read.

What the replica answers

A read is served locally only when the replica can serve it identically. The local reader imports the server’s own where and order-by compiler rather than restating it, and it declines anything outside a collection, a filter, a sort, and a limit — relationship expansion, free-text search, aggregates, and history all go to the server. An unrecognised key is declined too, so a query option added later cannot be silently answered as a different question.

The write path

Browser code declares the desired root graph with client.db.<collection>.mutate(values). The generated input type describes every writable nested relationship precisely. The server runs policies, approvals, hooks, root-and-relationship reconciliation, history, events, and audit through one canonical mutation pipeline; the root and every included relationship reconcile atomically. mutate returns Promise<void>, while the collection’s numeric pending value counts writes still in flight. Successful completion invalidates affected live queries; app code never calls invalidate, refetch, or revalidate itself.

  user action
        │
        ▼
  client.db.<collection>.mutate(values)
        │  collection.pending counts concurrent writes
        ▼
  server: policies → approvals → hooks → atomic graph reconciliation → audit
        │
   ┌────┴─────┐
   ▼          ▼
  committed  refused
   │          │
   │          ▼
   │     refuse() reason surfaces, nothing was written
   ▼
  drop this collection's cached answers, re-run the live
  queries reading it ──► Promise<void> resolves; pending decrements

One replica, many tabs

PGlite elects a leader across tabs through the Web Locks API: one tab holds the database and the others proxy their queries to it, so every tab reads the same rows. Only the leader opens the change stream and only the leader drains it, which is one connection per browser rather than one per tab. When it has applied a batch it announces the collection names over a BroadcastChannel, and the followers invalidate off that — a Postgres channel cannot be used here, because PGlite\u2019s worker clients share one session and one client\u2019s UNLISTEN disables every other listener. Leadership moving \u2014 the leading tab closing \u2014 hands the stream to whichever tab inherits it.

Completion, not a record

mutate never returns a record. A write-only or row-filtered policy may allow the caller to change a row it cannot read, so successful completion can promise only that the mutation committed. The live query owns whatever current data that caller is authorized to see.

The authored surface is Client ; the transport is Sync engine .