How Sync Works

An overview of how Jazz syncs data between clients, covering query subscriptions, infrastructure tiers, offline behavior, and eventual consistency.

Traditional apps send queries to a remote server, which queries a database and returns data.

This creates a few familiar problems:

  • the data is immediately stale
  • both the client and the server must be online
  • your app's performance depends on the speed of each network hop

Techniques like websockets, client-side caching, and optimistic updates help, but they add complexity without changing the basic request/response shape.

Jazz solves these problems with query subscriptions backed by a local replica.

Queries drive everything

When your app subscribes to a query (say, all todos where done = false), Jazz sends that query subscription upstream. The server evaluates the query against its own current relational state, finds the matching rows, and sends them back. The client only ever sees rows it has asked for (and has permission to read).

The set of active queries on a client defines the rows it can see and will keep receiving updates for.

The server keeps you subscribed

When you register a query subscription with the server, it remembers.

The server keeps a live query graph for that subscription. When local writes, remote replay, or schema/policy changes affect the result, it re-settles only the changed parts of the query and pushes the relevant row updates downstream.

That means:

  • if a new row matches your query, it is pushed automatically
  • if a matching row changes and no longer fits, it stops appearing in the subscription result
  • clients update their local replicas from deltas instead of from full snapshots every time

What happens offline

Reads always come from local storage.

In the browser, Jazz uses a dedicated SharedWorker reading from data stored locally in IndexedDB. Multiple tabs connect directly to that one durable runtime through message ports; there is no tab leader election.

Even without a network connection, your app can still read data it already has locally, whether that data was synced earlier or written locally on the device.

Writes behave the same way: they are applied locally immediately so the UI updates without waiting on a round-trip. Jazz queues the corresponding row-version updates for upstream sync. When the client reconnects, queued writes are sent and active query subscriptions are replayed automatically.

Infrastructure tiers

Jazz connects each client to Core, the server that authorizes and durably accepts writes.

Click New colour on any client to set a new colour for the row. It syncs to Core, which accepts the write and sends it to the other subscribed clients. If two clients write at once, the most recent write wins, and every node ends up showing the same colour.

Core
authoritative server
Alice
local
Bob
local
Charlie
local

Local is the client's copy of the data. In browser persistent mode, a worker owns that copy in IndexedDB. React Native uses a local native relay. These relays provide local persistence and connectivity; they do not replace Core's permission checks.

Core owns the authoritative database. A global write confirmation means Core has accepted and durably stored the write.

How data flows

app -> local persistence -> Core

A write first updates the app's local state. Jazz uploads it to Core, which checks permissions and either accepts or rejects it. Waiting for local confirms local persistence; waiting for global also requires Core acceptance. Neither choice changes whether the write is uploaded.

Reads sync on demand. An active query asks Core for the row versions needed to evaluate it, subject to the account's permissions. Core sends changes while the subscription remains active. The client evaluates queries locally using the data it has received.

Sync is automatic

Locally saved writes remain queued while disconnected. Jazz resumes uploading them and refreshes active query subscriptions when the connection returns.

Consistency model

Every write produces a new row version. Clients may temporarily disagree:

  • one client has an optimistic write that Core has not accepted yet;
  • another client has not received Core's latest update;
  • two clients have concurrent local writes.

Core settles writes and sends the authorized updates needed by each subscription. Local-first reads can use cached data and pending edits immediately. Strict remote reads wait for Core-authorized query coverage and exclude pending local edits.

When clients write concurrently to the same field of the same row, Jazz uses last-writer-wins (LWW) with deterministic hybrid logical clock ordering to decide the current visible result. Even row versions that lose that race remain in row history, so the system keeps enough information to reconcile deterministically and to support richer history-aware behaviour later.

A hybrid logical clock gives each write a timestamp made from the device's clock time and a counter. The counter moves the timestamp forwards when several writes happen in the same millisecond or when the device's clock has fallen behind an earlier write.

If two devices still produce the same timestamp, Jazz uses the ID of the writing node as a final tie-breaker. Once replicas have received the same updates, they choose the same winner regardless of the order in which the updates arrived.

See it in action

The examples reference shows complete applications that exercise query subscriptions, real-time sync, and conflict-friendly collaboration.