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>
65 lines
3.2 KiB
Markdown
65 lines
3.2 KiB
Markdown
# 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.
|