Skip to content

Sync & optimistic updates

datadata uses optimistic updates: every change applies to the local view immediately, while the server remains the single source of truth. Editing keeps working while you’re offline — changes replay on reconnect — but a change isn’t durably committed until the server accepts it.

  1. A client calls updateDocument. The change is applied to the local overlay instantly and sent to the server tagged with a client event id.
  2. The server validates the change (schema, guards, limits), assigns the document’s next sequence number, persists it, and appends it to the document’s event log. (The sequence versions the JSON snapshot only — Yjs deltas travel in their own lane, under their own cursor.)
  3. The server broadcasts the change to every subscriber of that document. The originating client recognizes its own event id in the broadcast and retires the optimistic entry — the local view and the confirmed view now agree.

If the server rejects the change — a failed guard, a validation error — the client discards the optimistic entry and the local view falls back to the confirmed server state. The UI sees a clean signal, not corruption.

getDocument returns the merged view: confirmed server state with any pending optimistic changes overlaid. Subscriptions deliver an initial full snapshot (doc:init) followed by incremental patches (doc:patch) — see the wire protocol.

The initial snapshot doesn’t have to travel over the socket: a server-rendered page can fetch documents over plain HTTP and hand them to the client as preloaded state. The subscription then carries the preloaded sequence, and the server confirms with no data transfer — unless changes arrived between the HTTP fetch and the subscribe, in which case it sends a fresh snapshot. Either way, embedded Yjs documents catch up with exact state-vector diffs, not re-sent states.

Some of this state lives in system documents — subscribable documents the client, server, or session maintains rather than user data, such as the client’s own sync bookkeeping (subscription states, pending optimistic counts, errors) that the devtools panel renders. They’re read like any other document; see System documents.

It hides latency and disconnects — but not durability

Section titled “It hides latency and disconnects — but not durability”

Optimistic updates mask network latency, and they carry over a disconnection: writes made while the socket is down are held locally and replayed on reconnect — structured (JSON Patch) writes last-writer-wins, the Yjs lane by CRDT self-heal (deltas merge cleanly no matter how late they arrive). The boundary is durability, not connectivity: by default that replay buffer lives only in memory, so a page reload drops anything still unsent. Opt-in offline persistence is what extends the buffer to disk — a journaled write survives the reload and replays on the next boot.

Replay is de-duplicated, not silently lossy

Section titled “Replay is de-duplicated, not silently lossy”

Re-sending a buffered write is safe to repeat: the server dedups by client event id, so a write that committed but whose acknowledgement was lost can be re-sent without applying twice. Dedup is time-bounded, though — a write left unconfirmed too long is dropped and reported as unconfirmed (an awaited write rejects, a fire-and-forget one hits onWriteError) rather than risking a silent double-apply. The exact rules — the replay horizon, sequence-guard exemptions, and the sweep that recovers a single lost frame on a still-live socket — are in Reconnect & replay.