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>
97 lines
4.4 KiB
Markdown
97 lines
4.4 KiB
Markdown
# 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 |
|