How sync works
Events are committed locally
When your app calls
store.commit(event), the event is immediately applied to local state as a pending event. The UI updates without waiting for the network.Events are pushed to the sync backend
The client leader thread pushes pending events to the sync backend. Before pushing, any upstream events that arrived since the last push must be pulled and applied first (similar to
git pull --rebase before git push).The backend assigns a global sequence number
The sync backend validates the event, appends it to the canonical event log, and assigns a global sequence number. It notifies other clients of the new event.
Nodes in the system
LiveStore uses a three-tier hierarchy:| Node | Role |
|---|---|
| Client session | A browser tab, window, or app instance. Holds pending events in memory. |
| Client leader | One session per client acts as leader. Owns the durable local event log (dbEventLog) and the local SQLite state (dbState). |
| Sync backend | Central authority. Stores the canonical append-only event log and broadcasts new events to clients. |
Happy path: committing an event
The following trace shows how a singletodoCreated event flows from a client session through to the backend.
1. Session commits event
1. Session commits event
The session merges the new event
e3 into its local SyncState as pending. It pushes the pending event to the leader thread. The leader still shows the previous head (e2) until it persists the event.2. Leader persists the event
2. Leader persists the event
The leader materializes the event into its local SQLite database and writes it to the local event log. The event remains unconfirmed because the backend has not acknowledged it yet.
3. Leader signals the session
3. Leader signals the session
The leader emits the event back to subscribed sessions. The session upgrades the event from pending to confirmed while the leader still awaits backend acknowledgement.
4. Backend acknowledges
4. Backend acknowledges
The leader pushes the event upstream. The backend validates, appends it, and acknowledges. All heads align.
Conflict resolution: rebasing
When two clients commit events concurrently, the sync backend assigns sequence numbers in the order it receives them. A client whose pending events lose the race must rebase its local work on top of the upstream events — similar togit rebase.
1. Divergence: client A has pending work, backend advances
1. Divergence: client A has pending work, backend advances
Client A holds pending event
A:e3 (todoRenamed). The backend has already committed B:e3 from client B.2. Leader pulls upstream and detects divergence
2. Leader pulls upstream and detects divergence
The leader compares its pending chain with upstream events and finds the divergence at
e2. It rolls back to e2.3. Authoritative events are applied
3. Authoritative events are applied
4. Pending events are replayed on top
4. Pending events are replayed on top
Client A’s original event is replayed with a new sequence number (
e4), on top of the authoritative head. Its payload is unchanged; only the sequence number is updated.5. Rebased event is pushed and confirmed
5. Rebased event is pushed and confirmed
The backend accepts
A:e4. All heads align.Rebasing is done on the client, not on the sync backend. This gives you full control over the rebase behaviour.
Event sequence numbers
Sequence numbers track causal ordering in the event log.| Field | Description |
|---|---|
seqNum | Globally unique integer assigned by the sync backend |
parentSeqNum | The sequence number of the parent event (establishes causal chain) |
| Field | Description |
|---|---|
global | Globally unique integer assigned by the backend (EventSequenceNumber.Global) |
client | Client-local counter; 0 for synced events, increments for client-only events |
rebaseGeneration | Increments each time the client rebases unconfirmed events |
e5 (global event 5), e5.1 (client-local), or e5r1 (after a rebase).
Client identity
Each client chooses a random 6-characterclientId (nanoid) as its globally unique identifier. In the rare event of a collision (detected when the client first pushes), LiveStore picks a new ID, patches local events, and retries automatically.
Sync heads
The latest event in an event log is the head (analogous toHEAD in Git). Because LiveStore uses hierarchical syncing, there are three heads to track:
- Session head — the latest event the session knows about
- Leader head — the latest event in the leader’s durable event log
- Backend head — the latest event in the canonical backend event log
Backend reset detection
When a sync backend is reset (e.g. deleting local Cloudflare state or wiping Postgres), clients with cached data may not know. LiveStore detects this using abackendId generated when the backend is first initialized.
Configure the behaviour with onBackendIdMismatch:
| Option | Behaviour |
|---|---|
'reset' (default) | Clears local storage and shuts down. The app restarts and syncs fresh from the backend. Recommended for development. |
'shutdown' | Shuts down without clearing local storage. The stale state persists until you manually clear it. |
'ignore' | Logs the error and keeps running in offline mode with stale data. |
Auth
Authentication and authorization for the sync backend is handled at the transport level. Add auth headers or tokens to your sync backend connection when initializing the adapter. Refer to your sync provider’s documentation for specific patterns.Design decisions
Why require a central sync backend?
Why require a central sync backend?
A central backend enforces a global total order of events. Without a canonical order, clients could permanently diverge. This means LiveStore is not suitable for fully decentralized or peer-to-peer use cases.
Why rebase on the client?
Why rebase on the client?
Client-side rebasing gives application developers control over the rebase process and keeps the sync backend simple and stateless with respect to individual clients. The backend only needs to append events and broadcast them; it does not need to understand business logic.
Why sync events rather than state?
Why sync events rather than state?
Syncing events (the event log) rather than the SQLite database means the sync layer is decoupled from the read model. You can change your SQLite schema and materializers without migrating the canonical log. The read model is always reconstructable from the event log.