Canvas example
Run the repository's multi-client canvas and trace presence, broadcasts, and persisted Yjs updates.
The repository includes a browser canvas for testing multiple realtime clients against a local API and worker. Unlike the demos elsewhere in this documentation, it runs against real infrastructure: Postgres, Redis, the API, and the realtime worker.
Start backing services
From the repository root:
pnpm install
cp .env.example .env
docker compose up -d postgres redis
pnpm db:migrate
pnpm db:seedStart the API and worker
Run each in its own terminal:
pnpm --filter @polynomialhq/api devpnpm --filter @polynomialhq/worker devThe API listens on http://0.0.0.0:4000 and the worker on ws://0.0.0.0:4001/v1/realtime. Both
expose a /health endpoint you can curl to confirm they are up.
Start the example
pnpm --filter @polynomialhq/example-canvas devOpen http://localhost:5173 in two browser windows.
Exercise the room
- Choose the same room ID in both windows.
- Select a different display name in each.
- Connect both windows.
- Move a pointer over the canvas to send presence.
- Add a block to persist a Yjs update.
- Reload one window and verify that the block returns from storage.
How authorization works
The example's development server uses PolynomialServer to issue a token:
const polynomial = new PolynomialServer({ secretKey });
const token = polynomial.authorizeRoom({
projectId,
role: "write",
roomId,
userId,
userInfo: { name },
});In production, the userId and the room permission must come from your application's
authenticated session, never from URL parameters. The example takes them from the query string
precisely because it is a smoke test.
How the browser joins
The example speaks the versioned WebSocket protocol directly:
socket = new WebSocket(config.realtimeUrl);
socket.addEventListener("open", () => {
socket.send(
JSON.stringify({
token,
type: "room.join",
v: 1,
}),
);
});Application code should normally use @polynomialhq/client or @polynomialhq/react. The raw
protocol is exposed here so the example can also act as a browser smoke test.
Presence flow
Pointer movement sends a presence.update message. Other clients receive
presence.updated, while a new connection receives presence.sync with the current room
snapshot.
Storage flow
The browser stores blocks in a Yjs document and sends encoded updates:
send({
encoding: "base64",
type: "yjs.update",
update: bytesToBase64(update),
v: 1,
});The worker persists the update before fanout. A new connection receives yjs.sync, applies the
persisted updates, and reconstructs the same block map.
Try a fresh room
Use New room to select a unique room ID. Both browser windows must join that new ID to share state. The previous room's persisted blocks remain isolated.
Scale-out check
With REDIS_URL configured, the worker fans room traffic out through Redis pub/sub so several
worker processes can serve the same room. The repository ships an integration test for that path:
pnpm --filter @polynomialhq/worker test:integrationRun it with Redis up to confirm cross-instance presence, broadcast, and storage delivery.