System Design: [Collaborative editor name]
Reference design for real-time collaborative document editing: several
people edit the same document at once, see each other's cursors, keep
working offline and merge cleanly when they reconnect. It compares
operational transformation (OT) with conflict-free replicated data types
(CRDTs), chooses a sequence CRDT, and specifies the sync protocol, presence,
offline merge, persistence with snapshots and compaction, access control and
local-first considerations. It follows the structure of the System Design
Document template and is pre-filled with a worked design to adapt. Values
marked Example are assumptions.
Document control
| Field | Value |
|---|
| Document ID | [SDD-COLLAB-001] |
| System / feature | [Real-time collaborative editing] |
| Version | [0.1] |
| Status | [Draft / In review / Approved / Implemented / Superseded] |
| Author(s) | [@author] |
| Reviewers | [@reviewer-1], [@reviewer-2] |
| Last updated | YYYY-MM-DD |
| Related | [PRD], [ADR: CRDT library], [Threat model], [Offline storage design] |
Approvals
| Role | Name | Decision | Date |
|---|
| Tech lead | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| Staff / principal engineer | [@name] | [Decision] | YYYY-MM-DD |
| Editor / client lead | [@name] | [Decision] | YYYY-MM-DD |
| SRE / operations | [@name] | [Decision] | YYYY-MM-DD |
| Security and privacy | [@name] | [Decision] | YYYY-MM-DD |
Revision history
| Version | Date | Author | Change summary |
|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft for design review |
| 0.2 | YYYY-MM-DD | [@name] | Example: Chose sequence CRDT over server OT to support offline editing |
| 0.3 | YYYY-MM-DD | [@name] | Example: Added snapshot compaction and history retention policy |
| 1.0 | YYYY-MM-DD | [@name] | Example: Approved |
Contents
1. Overview
1.1 Problem statement
[Example: Our editor saves whole documents with last-write-wins. When two
people edit the same document, one person's changes disappear; support
receives about 40 "lost work" tickets a week. Users also want to keep writing
on trains and planes and sync later.]
1.2 Proposed solution
[Example: Each document is a sequence CRDT stored on every client in local
storage. Edits apply locally at once and are encoded as small binary updates.
Clients exchange updates through a sync server over WebSocket; the server
authenticates, checks permissions, relays updates to other clients editing
the same document, and appends them to a durable update log. Periodically the
server merges the log into a compacted snapshot. Cursors and selections travel
on a separate ephemeral presence channel. Because merges are deterministic on
every replica, clients that were offline for days converge with everyone else
on reconnect without server-side conflict resolution.]
1.3 Decisions requested from reviewers
| ID | Question | Recommended option | Section |
|---|
| Q-01 | Example: OT or CRDT? | Sequence CRDT (YATA or Fugue family) via a mature library | 7.1, 10 |
| Q-02 | Example: Is the server a peer that merges, or a relay? | Relay plus persistence; merges only for compaction | 7.2 |
| Q-03 | Example: History retention? | Named versions forever; fine-grained history 30 days, then compacted | 7.6 |
| Q-04 | Example: End-to-end encryption? | Not in v1; design keeps the server content-agnostic except for compaction | 7.7 |
| [Q-nn] | [Question] | [Option] | [Section] |
2. Requirements
2.1 Functional requirements
| ID | Requirement | Priority |
|---|
| FR-01 | Example: Concurrent editing of the same document by up to 50 active editors | Must |
| FR-02 | Example: Remote edits appear without disrupting the local cursor | Must |
| FR-03 | Example: Presence: who is here, their cursor and selection, with colour and name | Must |
| FR-04 | Example: Offline editing for any duration, merged automatically on reconnect | Must |
| FR-05 | Example: Version history with named versions and restore | Should |
| FR-06 | Example: Roles: owner, editor, commenter, viewer; changes take effect immediately online | Must |
| FR-07 | Example: Undo and redo affect only the user's own changes | Must |
| [FR-nn] | [Requirement] | [Must / Should / Could] |
2.2 Non-functional requirements
| ID | Quality | Target | Measured as |
|---|
| NFR-01 | Local latency | Example: Keystroke to local render at or below 16 ms | Client performance traces |
| NFR-02 | Remote latency | Example: Edit visible to other online editors within 200 ms p95 (same region) | Sampled end-to-end probes |
| NFR-03 | Convergence | Example: All replicas identical after exchanging all updates | Property-based tests, checksum on sync |
| NFR-04 | Durability | Example: An update acknowledged by the server is never lost | Replicated log, restore drills |
| NFR-05 | Intent preservation | Example: Concurrent inserts at the same position never interleave character by character | Fuzz tests with known anomaly cases |
| NFR-06 | Document size | Example: Smooth editing up to 1 MB of text and 1 million lifetime operations | Benchmark |
| [NFR-nn] | [Quality] | [Target] | [Measurement] |
2.3 Out of scope
- [Example: spreadsheets and drawing canvases (other data types later)]
- [Example: comments and suggestions mode (next design, reusing the sync layer)]
- [Example: peer-to-peer sync without a server]
- [Item deferred, with the release that will take it]
3. Capacity estimation
3.1 Assumptions
| Input | Symbol | Value | Source |
|---|
| Concurrent editing sessions at peak | C | Example: 500 000 | Product analytics |
| Average sessions per open document | n | Example: 2.5 | Product analytics |
| Updates per session per second (batched every 50 ms while typing) | u | Example: 1 on average | Client telemetry |
| Encoded update size | su | Example: 100 bytes | Library benchmark |
| Presence messages per session per second (throttled) | p | Example: 2 | Client setting |
| Total documents | N | Example: 100 million | Storage inventory |
| Average compacted snapshot | sd | Example: 50 KB | Sample of documents |
3.2 Update traffic
Open documents and inbound updates:
Dopen=nC=2.55×105=200000 documents,Uin=C×u=500000 updates/s Each update is relayed to the other n−1 sessions on average:
Uout=Uin×(n−1)=500000×1.5=750000 updates/s Bin=Uin×su=50 MB/s,Bout=75 MB/s Presence adds C×p×(n−1)=1.5×106 small relayed
messages per second, never persisted.
3.3 Storage
Ssnap=N×sd=108×50 KB=5 TB Update log before compaction, if all inbound updates were kept for one day:
Slog/day=Uin,avg×su×86400 With an average of one third of peak, Uin,avg≈170000/s, so about
170000×100×86400≈1.5 TB/day before compaction.
Compaction (7.6) keeps the steady-state log small.
3.4 Summary
| Quantity | Estimate | Design consequence |
|---|
| Open documents | ~200 000 at peak | Route all sessions of a document to one sync node |
| Updates | ~500 000/s in, ~750 000/s out | In-memory relay per document; batched log appends |
| Presence | ~1.5 million messages/s | Separate ephemeral channel, throttled |
| Storage | ~5 TB snapshots, ~1.5 TB/day raw log | Continuous compaction into snapshots |
4. API design
4.1 Endpoints and messages
| Channel | Message | Purpose | Idempotent |
|---|
WebSocket /v1/docs/{id}/sync | sync1 (client to server) | Client state vector: what it already has | Yes |
| WebSocket | sync2 (server to client) | Updates the client is missing, plus server state vector | Yes, applying twice is harmless |
| WebSocket | update (both ways) | Incremental CRDT update | Yes, CRDT updates are idempotent |
| WebSocket | ack (server to client) | Update durably stored up to client clock | Yes |
| WebSocket | awareness | Cursor, selection, user info; ephemeral | Last-write-wins per client |
| HTTPS GET | /v1/docs/{id}/versions | Named versions and history points | Yes |
| HTTPS POST | /v1/docs/{id}/versions | Create named version | With Idempotency-Key |
| HTTPS PUT | /v1/docs/{id}/permissions/{principal} | Change role | Yes |
4.2 Message examples
Messages are binary in production; the JSON below shows their fields. The
state vector maps each client ID to the number of operations seen from it:
{"type": "sync1", "docId": "d_41K", "stateVector": {"1739201": 1820, "2210988": 455}}
{"type": "sync2", "docId": "d_41K", "update": "<binary: missing operations>", "stateVector": {"1739201": 1820, "2210988": 512, "3301207": 77}}
{"type": "awareness", "clientId": 2210988, "user": {"name": "Linh", "color": "#2f6fdd"}, "cursor": {"anchor": "<relative position>", "head": "<relative position>"}}
4.3 Errors and limits
| Code | Meaning | Client action |
|---|
forbidden | Role does not allow writing | Switch editor to read-only; keep local changes as a private draft |
doc-too-large | Snapshot exceeds limit | Show warning; suggest splitting |
rate-limited | Too many updates per second | Batch more aggressively |
reauth-required | Session token expired | Refresh token, reconnect, resync |
5. Data model
5.1 Entities
Server-side, a document is a snapshot plus an append-only log of updates since
the snapshot. Each client holds the same structure in local storage.
5.2 Storage choices
| Data | Store | Key | Main queries | Why |
|---|
| Update log | Example: Wide-column or key-value store with ordered keys | (doc_id, seq) | Append; range after seq | High append rate, sequential reads per document |
| Snapshots | Object storage, metadata in database | (doc_id, up_to_seq) | Latest snapshot | Large blobs, cheap |
| Documents, permissions, versions | Relational database | doc_id | Permission check; listings | Transactions, queries |
| Client replica | IndexedDB (web), SQLite (mobile, desktop) | doc_id | Load on open; append updates | Offline-first |
| Presence | In memory on the sync node | doc_id, client_id | Broadcast to room | Ephemeral |
6. High-level design
All sessions of a document are routed to the same sync node, which holds the
document's in-memory room.
| Component | Responsibility | Scales by |
|---|
| Client CRDT replica | Apply local edits, encode updates, merge remote updates, persist locally | Per device |
| Router | Send every connection for doc_id to its owning sync node | Consistent hashing with bounded load |
| Sync node | Authenticate, authorise, relay updates and awareness, append to log, acknowledge | Documents per node |
| Compaction workers | Merge log into snapshots, garbage-collect where safe | Queue of documents over thresholds |
| Document API | Listing, permissions, versions, export | Stateless replicas |
| [Component] | [Responsibility] | [Scaling unit] |
7. Detailed design
7.1 OT versus CRDT
| Aspect | Operational transformation | Sequence CRDT |
|---|
| Core idea | Transform concurrent operations against each other so they apply in any order | Give every element a unique, ordered identity so merges commute |
| Server role | Usually central server orders operations | Any replica can merge; server optional for correctness |
| Offline editing | Long offline periods mean large transformation chains and frequent manual conflicts | Natural: merge any time, any order |
| Correctness proofs | Transformation functions are notoriously hard to get right for rich text | Convergence follows from the data type's design; libraries are fuzz-tested |
| Metadata overhead | Small document state | Element IDs and tombstones; mitigated by run-length encoding and compaction |
| Maturity for rich text | Long production history in centralised editors | Mature open-source libraries with editor bindings |
The design chooses a sequence CRDT because offline editing (FR-04) and
local-first storage are requirements, and because it removes the need for a
single ordering server per document.
7.2 Sync protocol (critical path)
Sync is a state-vector exchange followed by incremental updates. The same
protocol handles first open, reconnect after a network drop and reconnect
after weeks offline.
The client keeps every local update in its local store until the server
acknowledges it, so closing the tab before sync never loses work. Relaying
happens before or in parallel with the log append so that remote latency is
not bounded by disk; the ack that lets the client drop its pending copy is
sent only after the append is durable.
7.3 CRDT choice and document model
- Text: a sequence CRDT from the YATA or Fugue family, as implemented by
a mature library (for example Yjs, or Automerge for JSON-like documents).
Prefer an algorithm that avoids interleaving anomalies when two users insert
at the same position concurrently (NFR-05).
- Rich text or structure: a tree of maps and sequences (paragraphs as
sequence elements, attributes as maps) bound to the editor's schema through
the library's editor binding.
- Markdown sources: keep the Markdown text itself as the replicated
sequence, so the source stays canonical and the preview is derived.
- Identity: each client session gets a random client ID (32-bit or wider); operations
are identified by
(client_id, clock). Reusing a client ID across devices
is forbidden because it breaks uniqueness.
7.4 Merge semantics users will see
| Situation | Result |
|---|
| Two users type at different places | Both edits kept, no visible conflict |
| Two users type at the same position | Both insertions kept as contiguous runs, ordered deterministically |
| One user deletes a paragraph while another edits inside it | Deletion wins for the deleted characters; inserted characters survive as a fragment (library dependent), visible to both |
| Concurrent formatting of the same range | Attribute merge per character; last writer by operation ID wins per attribute |
| Offline user edits a document for a week | Merges on reconnect; history shows their changes grouped by author |
Undo is scoped to the local user's own operations (an undo manager that only
tracks local origins), so pressing undo never removes a collaborator's text.
7.5 Presence and cursors
Cursors are stored as relative positions (anchored to element IDs, not
character offsets) so they stay correct while text changes around them.
Awareness messages are throttled to 2 per second per client, carry a 30 s
timeout, and are never written to the log. When a client disconnects, its
presence is removed after the timeout or immediately on a clean close.
7.6 Persistence, snapshots and compaction
- The sync node appends updates in batches (every 20 ms or 64 KB) to the log
under
(doc_id, seq).
- A document is queued for compaction when its log exceeds Example: 1 000
updates or 1 MB. The worker loads the latest snapshot, applies the log,
writes a new snapshot with its state vector, then deletes log entries up to
up_to_seq.
- Fine-grained history (who typed what, when) is kept for 30 days through
periodic history snapshots; named versions are kept indefinitely.
- Tombstone garbage collection removes deleted content's payload while
keeping the IDs needed for merging. Full removal of deleted-element IDs is
only safe when no replica can still send operations referencing them, which
cannot be known for offline clients; the design therefore keeps the IDs and
relies on run-length encoding to keep them small.
7.7 Access control, consistency and local-first considerations
- Consistency model: strong eventual consistency. Any two replicas that
have received the same set of updates are identical, regardless of order.
There is no global order and no locking; reads are always local.
- Write authorisation: the sync node checks the role on connect and on
every update. Updates from viewers are rejected, and the client is told to
switch to read-only, keeping its local changes as a private draft rather
than discarding them.
- Revocation: removing access stops future sync immediately, but a
replica already on the user's device keeps the content it had. State this in
the sharing UI; it is a property of every offline-capable editor.
- Server-side deletion of a document deletes snapshots and log; clients
receive a tombstone on next connect and remove their replica after
confirming with the user that unsynced work exists.
- Local-first: the editor opens and works with no network; the server
adds sharing, backup and multi-device sync, but is not required to edit.
- End-to-end encryption (future): the protocol already treats updates as
opaque bytes except during compaction. With E2EE, compaction moves to
clients, which upload encrypted snapshots.
7.8 Client connection states
Clients expose their sync state in the UI so users know whether their work is
on the server.
8. Scalability and performance
| Concern | Approach | Limit before redesign |
|---|
| Open documents | Consistent hashing of rooms to sync nodes | Example: ~5 000 active rooms per node, ~40 nodes at peak with headroom |
| Very popular document (company all-hands notes) | Room handles hundreds of editors; viewers receive batched updates every 250 ms | ~500 concurrent editors per room |
| Large documents | Lazy loading of sub-documents (one per section or page) | Single sequence above ~5 MB |
| Log write rate | Batched appends per room | Store write throughput per partition |
| Reconnect storm after node restart | Room state reloaded from snapshot + log; jittered reconnect | Snapshot load time |
Latency budget for a remote edit (target p95 200 ms same region):
| Step | Budget |
|---|
| Client batching interval | 50 ms |
| Client to sync node | 40 ms |
| Authorisation check (cached role) | 1 ms |
| Relay to other clients | 40 ms |
| Merge and render on receiver | 10 ms |
| Headroom | 59 ms |
9. Reliability, security and operations
9.1 Failure modes
| Failure | Impact | Detection | Mitigation |
|---|
| Sync node crash | Rooms on that node drop; clients reconnect | Connection drop rate | Clients keep pending updates; router moves rooms; state rebuilt from snapshot + log |
| Two nodes host the same room (routing change) | Updates split between nodes temporarily | Room ownership lease conflict | CRDT merge makes split harmless; both append to the same log; lease resolves ownership |
| Corrupted update | Client cannot apply; document may fail to load | Decode errors | Validate on receipt; quarantine bad updates; restore from snapshot |
| Compaction bug | Snapshot differs from log replay | Checksum comparison of replay vs. snapshot | Keep log until snapshot verified; roll back to previous snapshot |
| Client local store evicted by browser | Unsynced work lost | Pending-update age telemetry | Request persistent storage; warn when offline changes are unsynced for long |
| [Failure] | [Impact] | [Signal] | [Mitigation] |
9.2 Security and privacy
| Threat | Control |
|---|
| Unauthorised edits | Role checked on connect and every update; server rejects viewer updates |
| Forged authorship (spoofed client ID) | Server records authenticated user per update; history shows server-recorded author |
| Malicious oversized or crafted updates | Size limits, decode in a guarded path, per-connection rate limits |
| Data on lost devices | Local store encryption where the platform supports it; remote sign-out clears replicas on next launch |
| Content leakage through presence | Presence only within the room; display name and colour, no email by default |
| [Threat] | [Control] |
9.3 Observability
| Signal | Metric or log | Alert threshold |
|---|
| Remote latency | Probe edit to receipt p95 | Above 200 ms for 10 min |
| Pending updates | Client-reported oldest unacked update | p99 above 60 s while online |
| Convergence | Checksums of document state after sync (sampled) | Any mismatch |
| Room load | Rooms and connections per node | Above 80% of limit |
| Compaction backlog | Documents above log threshold | Above 10 000 for 30 min |
9.4 Cost estimate
| Item | Unit cost | Quantity | Monthly cost |
|---|
| Sync nodes | [$/node-month] | Example: 60 | [$] |
| Update log store | [$/TB-month] | Example: a few TB after compaction | [$] |
| Snapshot object storage | [$/TB-month] | Example: 5 TB plus history | [$] |
| Compaction workers | [$/node-month] | [n] | [$] |
| Total | | | [$] |
10. Trade-offs and alternatives
| Decision | Options | Chosen | Why | What we give up |
|---|
| Merge approach | Lock paragraphs; last-write-wins; OT; CRDT | CRDT | Offline editing, no ordering server, proven libraries | Metadata overhead, tombstones |
| Server role | Authoritative OT server; relay + persistence; pure peer-to-peer | Relay + persistence | Access control, backup, simple scaling | Server still needed for sharing |
| Library | Build own CRDT; adopt mature library | Adopt | Correctness is hard; libraries are fuzz-tested with editor bindings | Dependence on library's roadmap and format |
| Persistence | Store full document on every change; update log + snapshots | Log + snapshots | Cheap appends, history, fast load | Compaction jobs to run and verify |
| Transport | HTTP polling; WebSocket; WebRTC data channels | WebSocket | Works through proxies, simple server fan-out | Server bandwidth for all relays |
| [Decision] | [Options] | [Chosen] | [Why] | [Cost] |
11. Delivery plan and risks
11.1 Milestones
| Milestone | Scope | Exit criterion | Target date |
|---|
| M1 | Local CRDT document with offline persistence, single user | No data loss across 10 000 fuzzed sessions | YYYY-MM-DD |
| M2 | Sync server, real-time collaboration, presence | Convergence checks clean in beta for 30 days | YYYY-MM-DD |
| M3 | Compaction, version history | Snapshot verification 100% for 30 days | YYYY-MM-DD |
| M4 | Migration of existing documents | All documents converted; old save path removed | YYYY-MM-DD |
11.2 Rollout and rollback
[Example: Convert documents lazily on first open in the new editor, keeping
the old representation for 90 days. Rollback exports the CRDT state to the old
format, which is lossless for content though not for fine-grained history.]
11.3 Risks
| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|---|
| R-01 | Example: Document metadata grows too large for long-lived documents | Medium | Medium | Run-length encoding, sub-documents, size monitoring | [@name] |
| R-02 | Example: Library format change breaks stored updates | Low | High | Pin versions; store format version; migration tests | [@name] |
| R-03 | Example: Users surprised that revoked users keep offline copies | Medium | Medium | Clear sharing UI; documentation | [@name] |
| [R-nn] | [Risk] | [L / M / H] | [L / M / H] | [Mitigation] | [@name] |
12. Appendices
12.1 Open questions
| ID | Question | Owner | Due |
|---|
| OQ-01 | Example: Do we expose "suggesting" mode in v1? | [@name] | YYYY-MM-DD |
| OQ-02 | Example: Which library for the native mobile apps? | [@name] | YYYY-MM-DD |
| [OQ-nn] | [Question] | [@name] | YYYY-MM-DD |
12.2 Glossary
| Term | Meaning |
|---|
| OT | Operational transformation: adjusting concurrent operations so they can be applied in any order |
| CRDT | Conflict-free replicated data type: data structure whose replicas merge deterministically |
| State vector | Per-client count of operations a replica has seen |
| Tombstone | Marker for deleted content kept so later merges stay correct |
| Awareness | Ephemeral presence state such as cursors and user names |
| Local-first | Software where the primary copy of data lives on the user's device |
| [Term] | [Meaning] |
12.3 References
| Reference | Use |
|---|
| Shapiro et al., "Conflict-free Replicated Data Types" (2011) | CRDT foundations |
| Nicolaescu et al., "Near Real-Time Peer-to-Peer Shared Editing on Extensible Data Types" (YATA, 2016) | Sequence CRDT in 7.3 |
| Weidner and Kleppmann, "The Art of the Fugue" (2023) | Non-interleaving sequence CRDT |
| Kleppmann et al., "Local-first software" (Ink and Switch, 2019) | Local-first principles in 7.7 |
| RFC 6455 The WebSocket Protocol | Transport |
Standard and licence note
Based on common industry practice and published research on collaborative
editing cited in 12.3. This is a reference design to adapt, with illustrative
numbers. Original text written for this collection. Template licence: MIT.