Skip to content

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
  1. Each browser opens wss://…/ws/project/:projectId?token=<jwt>.
  2. 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.
  3. Yjs sync messages flow bidirectionally; the server merges updates into the room's authoritative Y.Doc.
  4. Awareness updates (cursor moves) are broadcast without persistence.
  5. After a debounce window with no edits, the WS service writes affected files to disk under PROJECTS_ROOT/<projectId>/.
  6. When the AI edits a file (via the docore tool path through the API), the API:
  7. Writes the file directly to disk.
  8. Calls services/ws over INTERNAL_SECRET-signed HTTP to rebroadcast the new content into the room — so collaborators see the AI's changes appear in their editor.

Source map

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_URL and 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