Skip to content

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

wss://<host>/ws/project/:projectId?token=<jwt>

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.

See also