Collaborative Editing Architecture
Real-time collaborative editing using Yjs CRDTs β SSE transport for the built-in editor, plus an optional WebSocket sync server for y-websocket clients.
On this page
SveltyCMS utilizes Yjs CRDTs for conflict-free collaborative editing β no Hocuspocus dependency. Two adapter-node-compatible transports exist:
- SSE (primary for the built-in editor) β
SseProviderconnects to/api/eventsand syncs viaGET/POST /api/collaboration/yjs. - WebSocket (optional, for y-websocket-compatible clients) β a lightweight
ws+y-protocolsserver at/ws(yjs-sync-server), mounted by the custom production entryindex.cjs.
π The Single Source of Truth (SSoT) Model
Unlike traditional CMS editors that rely on periodic auto-saves, SveltyCMS treats the Y.Doc as the active source of truth during an editing session.
SSE /api/events + /api/collaboration/yjs
or WS /ws] B[User B] <--> T T <--> YD[Y.Doc] YD --"Sync"--> S[Svelte 5 Runes] S --"Auto-Save"--> DB[(Database)]
π Implementation Details
Yjs Integration
- CRDT core:
yjs. - Client service:
collaboration-service.svelte.tsβ Svelte 5$state/$derivedwrapper that owns theY.Doc, awareness, and the transport provider. Wired into the field editor (fields.svelte) when a collection setscollaboration.enabled: true. - Server state:
yjs-service.tsholds per-docIdYjs documents keyed by tenant; updates flow throughpubSub(yjs:update/yjs:sync).
Transport 1 β SSE (primary for the built-in editor)
- Full-state bootstrap:
GET /api/collaboration/yjs?docId=...returns the server document as base64. - Updates: clients POST base64-encoded Yjs updates to
POST /api/collaboration/yjs; the server applies them viapubSubβyjsServiceand echoes changes back through the SSE stream (/api/events). - Awareness (presence/cursors): throttled (10/s) POSTs to the same endpoint with
awareness: true;yjsServiceapplies the update and broadcastsyjs:awarenessover SSE β remote editorsβ cursors/presence update in real time. Clients apply remote state withorigin: "server", which the send-path skips (no echo loop). - Transport client:
SseProvider(src/services/collaboration/sse-provider.svelte.ts) β EventSource + batched POSTs.
Transport 2 β WebSocket /ws (optional)
- Server:
src/services/collaboration/yjs-sync-server.tsβws+y-protocolssync + awareness, multi-documenttenantId:docIdisolation, graceful shutdown. - Mounting: only via the custom production entry
index.cjs(Plesk/Passenger), which attaches the upgrade handler to its ownhttp.createServer. The default@sveltejs/adapter-nodeentry (node build/index.js) does not mount/wsβ useindex.cjsfor WebSocket deployments. - Clients: y-websocket-compatible clients connect to
ws://[domain]/ws?docId=...&tenantId=.... The built-in editor does not use this path (it uses SSE).
Auth (2026-08-10):
/wsupgrades are session-validated and fail-closed β the app registers an authenticator (ws-auth-registryβhooks.ws upgrade()) that reuses the HTTP session pipeline (LRU β store β Redis β DB, negative cache). Unauthenticated upgrades receiveHTTP 401. The tenant used fortenantId:docIddocument keys comes from the resolved session β a client-suppliedtenantIdquery param can never override it. Test mode accepts the standardx-test-secretbypass. If the server runs without the app (no authenticator registered), all upgrades are rejected.
Presence & Cursors
- Real-time remote cursor tracking with user-specific colors via the Yjs awareness protocol.
- βActive Editorsβ presence rendered inline in
fields.svelte. - Field-level focus highlights to prevent overlapping edits (
setFieldFocus).
RichText
TipTap/ProseMirror RichText integration rides the same Y.Doc via y-prosemirror / TipTap collaboration extensions (client-side packages; sync transport as above).
π οΈ Technical Components
| Component | Responsibility |
|---|---|
| collaboration-service | Client $state service β Y.Doc lifecycle, awareness, provider wiring (used by fields.svelte) |
| SseProvider | SSE transport: EventSource subscription + batched POST /api/collaboration/yjs updates |
| yjs-service | Server-side per-docId Y.Doc store, applied from pubSub updates; GET /api/collaboration/yjs state |
| yjs-sync-server.ts | Optional ws + y-protocols WebSocket server at /ws (mounted by index.cjs) |
| Yjs Binding | fields.svelte reconciliation of Y.Doc updates with collectionValue |
βοΈ Configuration
Collaborative editing is built-in but can be toggled per collection:
- Enable Collaborative: Full CRDT-based multi-user editing (SSE transport).
- Strict Locking: Classic field-level locking (one editor at a time).
- Disabled: Standard save/overwrite behavior.
Data Integrity: Yjs ensures that even if a user goes offline, their changes are merged mathematically correctly once they reconnect, preventing the βLost Updateβ problem prevalent in REST-only systems.
π Performance & Scalability
Collaboration runs on standard Node.js WebSockets (ws) or plain HTTP SSE β no native addons, so resource profiles are predictable:
- Idle connections (memory-bound): ~15β30 KB RAM per socket (estimate) β ~15,000 idle clients per 512 MB container.
- Active editors (CPU-bound): a typing editor generates roughly 1β2 sync updates per second; throughput is bounded by the sync serverβs update rate.
- Active low-frequency clients: supports tens of thousands of concurrent clients at low update rates.
Latency/throughput figures for collaboration are not yet part of the recorded benchmark suite (tests/benchmarks/) and are not claimed here. The HTTP hooks pipeline is measured separately β see the Performance Benchmarks and Technical Evaluation.