Initial deliverable of the architecture phase: 16 ADRs (stack, CRDT collaboration, plugin sandbox, import/export, backups, CI/CD), data model, permission model, real-time collaboration and plugin concepts, deployment/operations/security documentation, and the milestone roadmap that the implementation issues are derived from. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3.2 KiB
3.2 KiB
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 aY.XmlFragmentbound 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-indexeddbpersists 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.