Guide

Requirements: many users edit one rich-text document at the same time. Local typing must feel instant, under 100 ms. People see each other's cursors. Users keep working briefly offline. The system keeps version history. Each piece below answers one of these.

Concept

Before you start. A CRDT document is a shared document that merges edits from any replica. A WebSocket is a two-way connection that stays open. A snapshot is a saved copy of the whole document at one point, so loading does not replay every edit.

The big idea

Each device keeps a full copy of the document. The device applies every keystroke to its own copy first, then sends the change to a server. The server relays it to the others and saves it. Because the merge rules converge, the copies agree once they have the same updates.

The copies do not agree on everything. Anything that needs one global answer, such as a unique document slug, still goes through a normal transactional API.

Walking through it

  1. Client. The editor applies each keystroke to a local CRDT document, so typing is instant. Updates are saved to IndexedDB, so the document survives a refresh and works offline.
  2. Transport. All clients editing one document connect to the same server instance. Route by document ID with consistent hashing, so the document's room lives in one process. See real-time delivery.
  3. Sync server. It relays updates to the room and appends them to a durable log, keyed by document ID, before it acknowledges. A client that reconnects sends the updates the server lacks, and receives the ones it lacks.
  4. Presence. Cursors are ephemeral. Broadcast them, but do not store them. Losing one is harmless.
  5. Storage. A compactor merges the log into a snapshot in object storage and trims the log. Loading a document means reading the latest snapshot, then the log tail.
  6. Permissions. The server checks that each update comes from a user with edit rights. Billing and unique names go through a transactional API.

Common mistakes

  • Sending every keystroke and waiting for the server. Typing then lags by a network round trip.
  • Never compacting the log. Loading a long document replays years of edits.
  • Storing cursors as durable data. It adds writes for no benefit.

Check yourself

  1. The sync server dies mid-session. Is any edit lost? No. Clients still hold their updates, and the log holds the rest. A client reconnects to another server and exchanges state to fill the gap.
  2. Why can a reconnecting client send its updates in any order? CRDT updates commute and are idempotent, so the result is the same whatever the order.

Key takeaways

  • Apply edits locally first, so typing feels instant.
  • Route each document to one server, so its room lives in one place.
  • Keep an append-only update log, and compact it into snapshots.
  • Keep ephemeral presence out of storage.
  • Use transactions for anything that needs one global answer.

Further learning

Local-first software argues that apps should keep the primary copy on the user's device and sync in the background.

Local-first software (Ink & Switch)A new generation of collaborative software that allows users to retain ownership of their data.inkandswitch.com

The Yjs documentation covers the document model and sync providers used in this design.

Yjs documentationModular building blocks for building collaborative applications like Google Docs and Figma.docs.yjs.dev

For the ordering side of multi-user state, see the chat system case study.