dorfteich/docs/architecture/adr/0003-yjs-crdt-collaboration.md
Claude Fable 5 0629411966 Add architecture documentation, ADRs, and operations concept
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>
2026-07-04 14:36:16 +02:00

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 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.