WebSocket Protocol¶
The services/ws server hosts one room per project. Clients are typically the Doable web editor, but any Yjs-aware client can connect.
URL¶
For local dev: ws://localhost:4001/project/:projectId?token=<jwt>.
The token is a regular Doable JWT. The WS server validates it against the API (or decodes locally with JWT_SECRET) and looks up the user's role on the project.
Subprotocol¶
The wire protocol is the standard y-websocket binary protocol:
| Message type | Direction | Meaning |
|---|---|---|
messageSync (0) |
both | Yjs sync round-trip (state vector + updates) |
messageAwareness (1) |
both | Awareness state (cursor, selection, presence) |
messageAuth (2) |
server → client | Auth-related signaling |
messageQueryAwareness (3) |
client → server | Bootstrap awareness on connect |
You normally don't read this layer directly — use y-websocket on the client and let it handle framing.
Frontend usage¶
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
const ydoc = new Y.Doc();
const provider = new WebsocketProvider(
process.env.NEXT_PUBLIC_WS_URL!, // ws://localhost:4001
`project/${projectId}?token=${token}`, // path
ydoc,
);
const yFiles = ydoc.getMap("files"); // Map<path, Y.Text>
// Bind a Y.Text to Monaco
import { MonacoBinding } from "y-monaco";
const yText = yFiles.get("src/App.tsx") as Y.Text;
new MonacoBinding(yText, monacoModel, new Set([editor]), provider.awareness);
Document shape¶
The room's root Y.Doc contains:
files: Y.Map<string, Y.Text>— keyed by relative file path.meta: Y.Map<string, any>— non-content metadata (last save, build status, etc.).
Awareness state¶
Each client publishes:
{
user: {
id: string,
name: string,
color: string, // hex, used for cursors
avatarUrl: string | null,
},
cursor: {
file: string,
selection: { start: number, end: number } | null,
} | null,
}
Server → API hooks¶
When a room is mutated, the WS server may call back to the API over INTERNAL_SECRET-signed HTTP for:
POST /internal/projects/:id/files-changed— debounced; triggers thumbnail regeneration and analytics events.POST /internal/projects/:id/presence— sends presence updates to non-room consumers (the workspace dashboard "active now" pill).
Disconnect & reconnect¶
y-websocket reconnects automatically with exponential backoff. On reconnect it sends a state vector; the server replies with the missing updates so the client converges to the latest state.