Sync Anchors & Delta Catch-Up
A client that goes offline and reconnects needs to recover lost state without re-downloading the entire database, and it needs a way to know when it does not need to download anything at all. Flux records the moment each channel was last confirmed current, asks your backend only what changed after it, and treats a confirmed empty answer as proof the channel is up to date.
Re-fetching entire datasets on every reconnect scales poorly and introduces state race conditions. Delta catch-up reduces recovery to one per-channel question: "What changed after timestamp X?" The answer is either rows to apply, a confirmation that nothing changed, or an honest "I cannot tell". Only that last answer ever costs a refetch.
Two records: the anchor and the coverage record
SyncAnchorMap. Anchors live in memory, so they are rebuilt after a reload.The coverage record
confirmedThrough is server time, because it is sent to the backend as the "since" of a catch-up and must be comparable with server timestamps. Everything else is device time, only ever compared against other values written by tabs on the same device, so a wrong client clock cannot corrupt liveness checks.- Confirmed: a bootstrap, a selective refetch, or a catch-up that the backend answered sets the confirmed time. This happens only after the data reached disk.
- Live: once a channel is confirmed and its socket is live, the leader tab claims it. While the claim holds, the leader writes a heartbeat (15 seconds by default), and each beat advances the confirmed time. A channel with a live claim and a fresh heartbeat is covered.
- Gap: when the network drops or the socket drops, the claim ends at that instant and the confirmed time is advanced to it, since everything up to then was delivered. The gap and its reason are persisted, so closing the tab while offline loses nothing.
- Crashed or closed leader: a leader that disappears cannot close its claim. Its heartbeat simply stops, and the next leader to be elected converts the orphaned claim into a gap that starts at the last heartbeat, so the catch-up begins at exactly the right point.
A gap of catchup-failed or too-old is a data problem, not a connectivity state. Going offline or losing the socket again does not overwrite it. Only a successful confirmation, normally a refetch of that channel, clears it. A channel can therefore never drift back to "looks fine" while a real question about it is still unanswered.
How adapters transmit sync anchors
- WebSocket: Encodes anchors in the initial URL query (
?syncAnchors=...) for eager catch-up, then executes an explicitSYNC_REQUESThandshake frame after subscription confirmation. - Socket.IO: Emits
FLUX_SYNC_ANCHORon connect, followed by a channel-scopedflux_sync_requesthandshake. - SSE: Appends
?syncAnchor=<unixMs>directly to the stream endpoint URL and processes backfill events upon connection. - Supabase: Invokes the
flux_request_deltaRPC function over Postgres Realtime.
The adapter contract
Return confirmed-current only when your backend positively answered. If your table has no changelog trigger, the function is missing, you lack permission to read the log, or the request timed out, return failed or declare delta: false. An empty reply that merely arrived is not a confirmation, and an adapter that reports one anyway can leave a channel permanently "up to date" while it silently misses changes.
What Flux does with each outcome
Applied or confirmed current. The channel is confirmed through the answer's asOf time and marked live. If the adapter gives no asOf, Flux uses the request start on the server clock, backed off by a few seconds so a later catch-up can never start after a change it missed. No refetch.
Too old. Either the adapter says so, or the gap is longer than the retention window it declared. Rows are not requested, since an empty answer would be meaningless. The channel opens a sticky gap and is refetched.
Failed, or nothing returned. Coverage is not advanced, the cache entry is not restamped fresh, and the channel opens a sticky gap that a refetch resolves. A sync() that returns nothing is treated as unverified: Flux does not assume it worked.
The Single-State Changelog Architecture
FluxChangelog).Every code sample on this page past this point is illustrative, shown in TypeScript and Node because that's what the Flux client and reference servers use — but nothing about the server side requires Node or JavaScript at all. Flux's adapters only care about the wire contract: specific event names for Socket.IO, specific JSON frame shapes for raw WebSockets, and specific query-string parameters for SSE. Whatever language your backend runs in — Python, Go, Ruby, Java, PHP — can implement the same single-state changelog pattern and query contract.
Supabase / Postgres Delta catch-up
table. A registration without one is TTL-only.flux_request_delta_batch call when that function is installed, and falling back to one flux_request_delta call per table otherwise), and fans the returned rows out to each channel that asked. Rows that arrive are applied. A reply that arrives with no rows is reported as confirmed current. A channel whose table is not subscribed reports failed with not-subscribed, an RPC error reports failed with rpc-error, and a reply that never arrives reports failed with timeout after 30 seconds.A table with no flux_changelog_trigger attached returns an empty reply every time, and an empty reply is read as "nothing changed". Attach the trigger to every table you register for realtime, and keep deltaRetentionMs equal to your changelog cleanup retention so a gap past it is refetched instead of trusted.
Socket.IO Delta catch-up
flux_sync_request for each channel and expects a flux_sync_complete response payload containing the array of delta rows from queryChangesSince.WebSocket Delta catch-up
action: 'SYNC_REQUEST' and expects action: 'SYNC_COMPLETE' carrying the backfill rows.SSE Delta catch-up
req.query.syncAnchor, executes queryChangesSince, and streams backfill events down the open response body immediately after client registration.MongoDB Change Stream Catch-Up
FluxChangelog collection. Catch-up queries scan this collection to return deduplicated delta rows.Everything beyond subscribe and unsubscribe is optional on the adapter contract, and each missing piece degrades gracefully. With no delta support, a channel is realtime-only: live events keep it current, but any gap is closed by refetching that channel. With no realtime at all (the noop adapter, or a registration with no table on a table-based backend), a channel is TTL-only. An adapter that returns nothing from sync() is treated as unverified: Flux cannot prove the channel current, so it refetches it rather than guess. Delta catch-up is an enhancement a transport opts into.
For complete backend setups, refer to getting_started/websocket, getting_started/socketio, and getting_started/sse. For how catch-up results are tracked, see core_concepts/activity_bus.