# 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 ```mermaid 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 1. **Open**: client fetches a collab token from the API (permission check happens here), connects to `/collab` with it. Read-only users connect in `ro` mode: they receive updates and awareness but the server drops any update they send. 2. **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. 3. **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 the `page_links` wikilink index — search and backlinks are therefore near-real-time. 4. **Close**: when the last participant disconnects, the server persists finally and triggers an automatic version snapshot if content changed (ADR 0013). ## Offline behavior - `y-indexeddb` keeps 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 `ro` token; 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; `maxDocuments` and 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_updates` into `ydoc_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 |