# ADR 0003: Yjs CRDT + Hocuspocus for real-time and offline collaboration - Status: accepted - Date: 2026-07-04 ## Context The product vision requires: simultaneous editing with live cursor positions of all participants, live-synced input, **and** (decided in kickoff) offline editing with conflict-free merge on reconnect. Expected concurrency is small groups per page. This rules out naive locking and makes Operational Transformation (OT) unattractive: OT needs a central authority and handles offline divergence poorly. CRDTs are the established answer for offline-capable collaborative editing. ## Decision - **Yjs** is the CRDT implementation. Page content is a Yjs document (`Y.Doc`) containing a `Y.XmlFragment` bound to the editor (ADR 0004). - **Hocuspocus** (the Yjs WebSocket server by the TipTap team, MIT) is the collaboration server, running as its own container `apps/collab`: - `onAuthenticate`: validates a short-lived collaboration token (JWT) issued by the API; the token encodes user id, page id, and access level (read-only vs. read-write). No token, no connection. - `onLoadDocument` / `onStoreDocument`: loads and persists document state to PostgreSQL (debounced writes; merged state plus an update log with periodic compaction). - Awareness protocol carries cursor positions, selections, and user display info for the live-cursor UI. - **Offline support** on the client: - `y-indexeddb` persists every opened document locally; edits while disconnected accumulate in IndexedDB and merge automatically on reconnect (CRDT property — no conflict dialogs). - The SPA is installable/cachable as a PWA (service worker caches the app shell) so the editor loads without a network connection. - Read-only permission is enforced server-side: the collab server rejects updates on read-only connections; offline edits by users whose write permission was revoked are rejected at sync time and the client informs the user. - **Cursor display**: TipTap's collaboration-cursor extension renders remote cursors/selections from awareness states. - Version history hooks into this layer via Yjs snapshots (ADR 0013). ## Consequences - Conflict-free merging is guaranteed by construction; no merge UI needed. - Document state in PostgreSQL is binary (Yjs update format). For search, export, and web rendering, the API maintains a derived, plain representation per page (see `data-model.md`, `page_content_cache`), refreshed by the collab server's store hook. - The collab server is stateless apart from in-memory open documents; it can be restarted at any time (clients resync). Horizontal scaling would need sticky routing or Redis pub/sub — out of scope at target size (ADR 0002). - We accept the Yjs storage overhead (tombstones) — mitigated by snapshot compaction (ADR 0013). ## Alternatives considered - **Operational Transformation (e.g. ShareDB)**: mature for online-only editing, weak offline story. Rejected because offline is a requirement. - **Automerge**: viable CRDT, but the editor-binding ecosystem (ProseMirror/TipTap) and server tooling around Yjs are significantly more mature. - **Self-written sync protocol**: never a good idea for this problem class.