examples/live-notes Public
---id: wire-protocoltype: conceptowner: justinreviewed: 2026-10-15component: opsdescribes: - src/protocol.ts---# Wire protocolEditors and a room exchange JSON text frames over the WebSocket at `/notes/:id/ws`. Versionsare the room's: the number of ops it has applied to the note.From an editor:```json{ "type": "op", "base": 41, "op": { "type": "insert", "pos": 4, "text": "oat " } }{ "type": "op", "base": 41, "op": { "type": "delete", "pos": 0, "count": 3 } }```From the room:| type | fields | when || ---------- | --------------------------- | --------------------------------------------------- || `snapshot` | `text`, `version`, `reason` | on connect, and when an editor's op can't be used || `ack` | `version` | your op was applied, or turned out to change nothing || `op` | `version`, `op`, `by` | someone else's op, already transformed || `presence` | `here`, `you` | someone joined or left; `you` only to the newcomer || `error` | `message` | the frame was not an op message |`reason` is only set when the room refused an op the editor had already applied, for exampleone that would take the note past 100,000 characters. The page shows it in the status line.`by` is the editor id of whoever made the op, and `here` lists editors as`{ "id", "name", "color" }`, oldest connection first. Editors never send presence; the roomworks it out from the sockets ([[presence]]).`parseClientMessage()` checks every field and copies only the ones it knows, so a frame withextra fields still works and a malformed one is answered with `error` and changes nothing.An editor sends its next op only after the ack for the previous one; see [[conflict-strategy]]for why that keeps the room's job simple.