Skip to content

Sync engine

Sync engine

Bolt ships a native sync engine — not a plugin, bolt-on cache, or third-party replication layer. This page is how it works under the hood: the replica, the change feed, and the transport. For the read and write API apps are authored against, see Live data.

The mental model: your browser is a replica

Think of it as having a second copy of your database running inside the browser — one that belongs to the person signed in. Your app reads from that copy like a local database, writes to it like a local database, and the engine keeps it in step with everyone else in the organization.

        SERVER DATABASE (Postgres)                    THIS BROWSER
        ┌────────────────────────────────┐            ┌────────────────────────────┐
        │ collections                    │  snapshot  │ PGlite replica in          │
        │ policies · hooks               │ ─────────► │ IndexedDB — same tables,   │
        │ approvals · audit              │            │ same rows, filtered to     │
        │ bolt_sync_outbox (change log)  │  sync.diff │ THIS user's policy scope   │
        └────────────────────────────────┘ ─────────► └────────────────────────────┘
              ▲                                            ▲
              │  client mutation pipeline                  │
              └────────────────────────────────────────────┘

              the write path also publishes on the `bolt.sync` topic;
              the host fans that out to the tenant's open connections,
              and a replica that hears it asks for the changes it names

Two flows keep the copy honest. Down: the write path tells the host something changed, the host wakes the browser, and the replica asks for the committed changes and applies them. Up: your writes travel as ordinary collection commands and are committed, or refused, by the server, which stays the authority.

On Colony , the server side of this runs in your tenant runtime behind a proxied HTTP transport; the engine itself is a Bolt concern and works the same on any host.

How the replica stays current

The server is never polled on a timer. Every committed mutation also writes a row to the change log ( bolt_sync_outbox ) in the same transaction, and once that transaction commits the write path publishes the collection names on a topic the host fans out:

  someone commits a mutation
        │  row written to bolt_sync_outbox in the SAME transaction
        ▼
  COMMIT  ──►  publish on `bolt.sync`  ──►  host fans it out to the
                                            tenant's open connections
                                    │
                                    ▼
     GET /api/bolt/sync/stream   event: sync   (the collection names)
                                 event: ready  (connected, or reconnected)
                                    │
                                    ▼
              the leading tab calls sync.diff from its cursor and
              applies the batch — create | update | delete | reset —
              then re-runs the affected live queries ──► UI updates
  • The stream carries a durable cursor of transaction id and sequence, so a dropped connection resumes where it left off. The log is only ever read below the oldest transaction still in flight, which is what turns an insert-ordered table into a commit-ordered stream: a client is delayed by an open write, never robbed of one.
  • If a client is too far behind — its cursor sits below what compaction retained — the batch it gets back is a single reset change, and the replica rebuilds itself from a fresh snapshot.
  • Every batch is filtered by the reader’s policy scope in SQL before it is sent, and columns the subject may not read are masked by the same rule a direct read goes through. The identity tables — user and the session, account, and verification tables beside it — are excluded from replication outright, however broad the reader’s grants are.
  • An idle stream costs nothing, because nothing is on a timer: the connection is quiet until a write publishes on the topic.

First load, reload, and multiple tabs

  FIRST VISIT                      RELOAD / SECOND TAB
  ───────────                      ───────────────────
  sync.provisioning builds        tables are already warm —
  the tables from the             first frame renders from
  tenant's own migrations         local data, no spinner
        │                         │
  sync.snapshot pages every       the leading tab reopens the
  readable collection             stream and drains from the
        │                         persisted cursor
  the stream subscribes at        │
  the cursor the snapshot         live queries re-run, UI
  handed back                     catches up in the same
                                  frame as any new changes
  • The replica renders no DDL of its own. It is provisioned from the tenant’s own migration lineage, so a column’s type in the browser is the column’s type on the server rather than a mapping that agrees with it today.
  • Storage is keyed by <tenant>::<environment>::<accessScope> , so one browser signed into two workspaces never points both at the same tables — including two workspaces built from the same template, where the schema fingerprints match and nothing else would have caught it.
  • One tab owns the database and the rest proxy to it, elected through the Web Locks API . Only the leader streams and only the leader drains, so a browser holds one connection rather than one per tab; after each applied batch it announces the collection names to the others through the database they share.
  • Because the replica and its cursor persist in IndexedDB, a reload re-opens it already populated and serves the first paint from local data — then resumes the stream at the saved cursor.

Which reads the replica answers

Holding the rows is not the same as answering the question. The replica answers a read only when it can answer it identically, and it decides that by the shape of the query:

  • Answered locally — a collection, a filter, a sort, and a limit. These are compiled by the server’s own where and order-by compiler, imported rather than reimplemented, so the local path produces the same SQL and cannot drift into a second opinion about what a filter means.
  • Sent to the server — relationship expansion, free-text search, aggregates, and history. Those live in the collection runtime rather than in the compiler, so the replica does not attempt them.

A query option the local reader does not recognise is declined rather than ignored, which is what keeps a later addition to the query surface from being quietly answered as a different question.

Not the same as bolt sync or Studio preview sync
Bolt sync engine — the live data queries and collection writes in running tenant apps.
bolt sync CLI — filesystem compiler sync that generates .norbital/ types and registries before build.
Workspace Studio preview sync — release artifact build that applies DDL and records an immutable release artifact. See Workspace Studio.

What happens over the wire

The sync engine has no routes of its own. It is a set of commands on the same channel every other Bolt command travels on, /api/bolt/command/<command> , plus one stream the host holds open at /api/bolt/sync/stream that carries collection names and nothing else — no rows, no cursor, no operation:

CommandPurpose
sync.provisioningThe ordered DDL this tenant was provisioned with, plus the schema fingerprint and the collection shape
sync.shapeThe collections this subject may replicate
sync.snapshotOne keyset page of one collection — { collection, rows, cursor, nextAfter }
sync.headThe newest cursor the log can be read to
sync.diffThe changes after a cursor, policy-filtered and column-masked, in commit order
sync.compactCollapses superseded log rows and prunes past the retention window

A cursor below what compaction retained is answered with a single reset change, and the client rebuilds its replica from a fresh snapshot. The full protocol is described in the public Bolt sync-engine documentation .

  • Live data — the live queries and collection writes authored against this engine
  • Bolt framework — where the sync engine sits in the platform
  • Policies — scope what reaches the local replica