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>
4.4 KiB
4.4 KiB
Real-time collaboration
How live editing, cursors, offline work, and persistence fit together. Foundational decisions: ADR 0003 (Yjs + Hocuspocus), ADR 0004 (TipTap), ADR 0013 (versions).
Components
sequenceDiagram
participant E as Editor (TipTap + Yjs)
participant I as IndexedDB (y-indexeddb)
participant A as api (REST)
participant C as collab (Hocuspocus)
participant P as PostgreSQL
E->>A: GET /pages/:id/collab-token
A->>A: resolve permissions (shared lib)
A-->>E: JWT {userId, pageId, mode: rw|ro, ttl 60s}
E->>C: WebSocket connect (token)
C->>C: onAuthenticate: verify JWT
C->>P: onLoadDocument: state + updates
C-->>E: initial sync (Yjs protocol)
E->>I: persist locally (continuous)
E->>C: updates + awareness (cursors)
C-->>E: other participants' updates/awareness
C->>P: onStoreDocument (debounced)
C->>P: refresh page_content_cache, page_links
Document lifecycle
- Open: client fetches a collab token from the API (permission check
happens here), connects to
/collabwith it. Read-only users connect inromode: they receive updates and awareness but the server drops any update they send. - Edit: Yjs syncs deltas both ways; TipTap renders remote changes; the collaboration-cursor extension renders remote cursors/selections with each participant's display name and a stable per-user color.
- Persist: Hocuspocus stores the merged state to PostgreSQL, debounced
(default 2 s after last change, hard interval 30 s). The same hook
refreshes
page_content_cache(plain text, Markdown, HTML, outline) and thepage_linkswikilink index — search and backlinks are therefore near-real-time. - Close: when the last participant disconnects, the server persists finally and triggers an automatic version snapshot if content changed (ADR 0013).
Offline behavior
y-indexeddbkeeps every opened page's document local; the PWA service worker keeps the app shell loadable. A user can open previously-visited pages and edit without a connection.- On reconnect, Yjs's sync protocol exchanges state vectors; local and remote changes merge conflict-free. There is deliberately no conflict UI — CRDT semantics decide; version history is the safety net for surprising merges.
- Permission changes vs. offline edits: tokens are re-acquired on every
reconnect. If write permission was revoked while offline, the reconnect
yields an
rotoken; the client keeps the local changes visible, informs the user ("your edit permission was removed — export your changes"), and offers copy/Markdown export of the local version. Local state for a page is discarded when the user leaves it after a successful sync. - Creating new pages offline is out of scope for v1 (requires the REST API); editing existing ones is the offline use case.
Awareness (cursors & presence)
- Awareness state per participant: user id, display name, color, cursor anchor/head. The editor UI shows remote carets inline and an avatar strip of current participants at the top of the page.
- Awareness is ephemeral (never persisted). Read-only participants appear in presence but without a caret.
Server operations
- The collab server holds open documents in memory;
maxDocumentsand per-connection message size limits guard resources. It is safe to restart at any time — clients resync from IndexedDB + server state. - Update-log compaction (merge
page_updatesintoydoc_state) runs as a maintenance job when a page has > N log entries (default 500) and no open session. Version snapshots are self-contained, so compaction is invisible to history (ADR 0013). - Scaling note: one collab instance serves the target scale (ADR 0002). The sharding/pub-sub upgrade path (multiple instances + Redis adapter) is documented here as the known escape hatch and requires no data-model change.
Failure modes
| Failure | Behavior |
|---|---|
| collab container down | editing degrades to offline mode (local persistence); banner "reconnecting…"; REST reads unaffected |
| WebSocket blocked (proxy) | same as above; deployment docs require WebSocket pass-through for /collab |
| stale collab token | client transparently re-fetches and reconnects |
| document too large | server rejects updates beyond a size ceiling (instance setting) with a user-visible error; prevents runaway documents |