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

Design comments

Design comments are pin-based annotations on the visual preview canvas, modeled after Figma's comment system. They flow through the same WebSocket room as code collaboration but use a separate message channel.

Data model

Each comment is stored in design_comments (PostgreSQL) with:

Column Type Purpose
id UUID Primary key
project_id UUID FK to projects
user_id UUID Author
display_name text Author name shown on pin
user_color text Hex color for the pin
x_percent, y_percent real Position as percentage of canvas dimensions
selector text CSS selector of the target element (optional)
page_path text Route path the comment was placed on
content text Comment body
parent_id UUID Self-referencing FK for threaded replies
resolved boolean Whether the comment is resolved

Message flow

User A places comment
   ├─ WS: design-comment:add ──▶ services/ws ──▶ broadcast design-comment:added
   │                                    │
   │                                    └─▶ POST /internal/design-comments/:projectId
   │                                              │
   │                                              ▼
   │                                       PostgreSQL persist
   └─ User B receives design-comment:added ──▶ pin appears on their canvas

The same pattern applies for resolve, unresolve, and delete actions.

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 to 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