Real-time Collaboration¶
Multiple people can edit the same Doable project simultaneously — the same way Google Docs or Figma works. Cursors, selections, and edits sync in real time. The mechanism is Yjs CRDTs over WebSockets.
Pieces involved¶
| Piece | Role |
|---|---|
| Y.Doc | A CRDT document. One per Doable project. Holds a Y.Map of files, each containing a Y.Text for the file content. |
| Awareness | Yjs side-channel for ephemeral state — cursor positions, selections, user color/name. |
y-websocket (client) |
The Monaco editor uses y-monaco + y-websocket to bind a Y.Text to the editor model. |
services/ws |
Custom WebSocket server (Hono + ws) that hosts one room per project. |
services/api |
Authenticates connections, persists snapshots, notifies on AI-driven file writes. |
Wire-level flow¶
Browser A Browser B
│ │
│ WSS /ws/project/:projectId?token=… │
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ services/ws ── Yjs sync + awareness │
└────────────────┬─────────────────────────┬───────────────┘
│ persist on idle │ /internal/ws/notify
▼ ▼
PROJECTS_ROOT/… services/api ── recompute thumbnails,
broadcast file events
- Each browser opens
wss://…/ws/project/:projectId?token=<jwt>. - The WS service verifies the JWT against the API (or decodes locally with
JWT_SECRET) and looks up the project's room — creating it if it's the first connection. - Yjs sync messages flow bidirectionally; the server merges updates into the room's authoritative
Y.Doc. - Awareness updates (cursor moves) are broadcast without persistence.
- After a debounce window with no edits, the WS service writes affected files to disk under
PROJECTS_ROOT/<projectId>/. - When the AI edits a file (via the
docoretool path through the API), the API: - Writes the file directly to disk.
- Calls
services/wsoverINTERNAL_SECRET-signed HTTP to rebroadcast the new content into the room — so collaborators see the AI's changes appear in their editor.
Source map¶
- WS server:
services/ws/src/index.ts— Hono bootstrap and upgrade handler. - Message routing:
services/ws/src/message-handler.ts. - Rooms & presence:
services/ws/src/collaboration/,services/ws/src/rooms/. - Frontend binding:
apps/web/src/modules/editor/— Monaco +y-monaco+y-websocket. - AI → WS notification:
services/api/src/ai/yjs-bridge.ts.
Conflict resolution¶
CRDTs are conflict-free by definition: every operation is commutative and associative. Two simultaneous edits at the same position end up in a deterministic order, identical on every client.
This means:
- ✅ No locks. No "someone else is editing this file" modal.
- ✅ Offline edits sync automatically when reconnecting.
- ⚠️ It is theoretically possible for two users + the AI to all change overlapping lines at the same instant. The result is well-defined but may be semantically ugly — the editor never breaks, but the code may need a quick fixup. In practice this is extremely rare.
Persistence¶
- Live state lives in memory inside
services/ws. - Snapshots are written to
PROJECTS_ROOT/<projectId>/on a debounce. - History isn't kept by Yjs itself; for that, Doable uses the per-project Git store (
services/api/src/version-control/) — every meaningful change creates a commit you can browse and roll back from the UI.
Scaling notes¶
- A single WS process handles thousands of small rooms easily.
- For multi-replica deployments, set
REDIS_URLand Doable's KV store will share room → presence state across replicas. (Yjs sync itself is room-affine — sticky sessions or a Y-redis adapter is required to scale a single hot room across replicas; for typical workloads this is unnecessary.)
Disabling collaboration¶
If you only ever expect single-user use, you can:
- Skip running
services/ws(the editor falls back to local-only mode). - Set
NEXT_PUBLIC_WS_URL=empty — the editor will warn about presence being unavailable but otherwise work.
See also¶
- Architecture Overview.
- API → WebSocket Protocol — message types on the wire.