Skip to content

Client internals

Client internals

The client is the browser half of the runtime: a Machine reducer plus drivers. This page is what happens inside it — how live queries stay current over the sync HTTP/SSE session, how writes travel, and why the server remains the only authority. There is no browser PGlite replica and no IndexedDB tenant database. Web Locks elect one EventSource owner for the browser profile.

The Machine

The browser holds current query answers in memory only. machine.ts is a pure reducer: versioned prefixes, pending writes, and link status. Two drivers move bytes — one profile-local SSE stream in, serialized HTTP control and write calls out. A mounted live prefix is answered by sync.connect and kept current by apply frames. A prefix update applies only when the retained version equals the update’s fromVersion; otherwise the link restarts. A failed revalidation yields failed, never a confident stale answer. There is no second source of truth in the browser.

What a live query answers

findMany and findFirst with a contiguous limit are live — a prefix registered with the host and pushed thereafter. count, findGrouped, a cursored after page, and semantic search are one-shot: answered once over the transport and never filed live. The server is the authority on every answer: permissions and invariants stay server-authoritative. The browser does not compile local SQL, and it does not keep a policy-scoped replica.

The write path

Browser code submits a declared input with client.collection.<collection>.create(input), .update(id, input), or .delete(id). The Machine enqueues it and pushes it as one collections.write command with the connection header. Durability is memory — this tab’s queue — until the authority settles it. Each call resolves immediately with the optimistic row; the returned handle exposes settlement, status, and wait. project() overlays pending writes on the held answer so the UI updates same-frame. Settlements are accepted, rebased, rejected, or quarantined; nothing is claimed saved before its outcome. App code never calls invalidate, refetch, or revalidate itself.

  user action
        │
        ▼
  client.collection.<collection>.create(input)
        │  Machine queues the write (durability: memory)
        │  pending += 1; project() overlays the optimistic row
        ▼
  collections.write ──► server write pipeline
        │
        ▼
  apply frame: patches + settlement
  (accepted | rebased | rejected | quarantined)
        │
        ▼
  Machine step(); pending decrements

One EventSource per browser profile

Each tab owns its own Machine and in-memory write queue. One EventSource is shared across tabs and workspaces in the same browser profile — Web Locks elect the owner tab and BroadcastChannel fans frames out. There is no IndexedDB tenant database. Closing the owner tab hands the stream to another live tab in that profile; a new profile opens a new EventSource.

Optimistic overlay, then settlement

A write returns the optimistic row immediately so the UI can paint, not a durable server record. Durability stays memory until the settlement arrives. A write-only or row-filtered policy may allow the caller to change a row it cannot read, so the live query — re-evaluated on the server — owns whatever current data that caller is authorized to see.

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