Wire protocol
This page specifies the socket events, payload framing and REST conventions the server uses on the wire. Use it to build custom clients, debug traffic, or integrate from environments the official client extension does not cover.
Transport
Real-time traffic is Socket.IO. Clients connect with identifying query parameters:
| Query param | Meaning |
|---|---|
guid |
Document to join. The socket enters this document's room and receives its broadcasts. |
user |
Caller-chosen user identifier (a UUID in the official client). Echoed on presence ops; if absent, the server mints one for the connection. |
token |
Your authentication token, passed verbatim to your auth hooks. |
type |
The official client sends socket. REST requests reach the same hooks with type: 'api', so hooks can distinguish the transport. |
Any additional query parameters (the official client merges its auth option here) are forwarded verbatim to your hooks, and the server does not interpret them.
Connection authentication
Before a socket is accepted, the server runs your auth hook and then your beforeConnect hook with the full handshake query. If either returns a falsy value, the connection is rejected with the error Invalid authentication.
Payload framing
Every non-trivial payload, meaning operations and document configs, is a zip archive containing a single file main.json whose content is the JSON document. Compression is applied by both sides (JSZip on the official client). To decode a captured payload:
const zip = await JSZip.loadAsync(buffer);
const obj = JSON.parse(await zip.file('main.json').async('string'));
Socket events
JSS (client → server): apply an operation
Payload (zipped):
{
"user": "6f1c…", // sender's user id
"version": "0d9e…", // client-minted version guid for this change
"worksheet": "b2a4…", // worksheetId the op targets
"method": "setValue", // public worksheet method to apply
"args": [[{ "x": 2, "y": 4, "value": 1310 }]]
}
The server authorizes it (beforeChange), applies worksheet[method](...args) to the live instance, increments the document revision, persists via your change handler, and acknowledges:
{ "status": true, "rev": 42 }
worksheet may be a worksheetId string or a numeric worksheet index; the server resolves either. When beforeChange denies the operation:
{ "status": false, "error": 1, "message": "Permission Denied" }
Any thrown error (unknown worksheet, engine failure) acknowledges with { "status": false, "error": 1, "message": "Something went wrong" } and is reported to your error hook.
JSS (server → client): replay an operation
The original zipped payload, re-emitted to every other socket in the room, with the revision as a second argument:
socket.on('JSS', async function(zipPayload, rev) { ... });
Peers replay the op locally. rev is the server's monotonic revision after applying it. It is stamped on broadcasts and acknowledgements so clients can order operations and detect missed ones; the official client extension replays operations in arrival order and uses the version signature on reconnect instead (sync internals).
Writes made through the REST API are broadcast on the same event: the operation object is zipped and emitted as JSS with the revision, to every socket in the document's room.
create (client → server)
Zipped { guid, config }, where config is a JSON string of the spreadsheet configuration. Worksheets must each carry a worksheetName; missing worksheetIds, namespace and version are minted server-side. Acknowledged with the adapter's result (e.g. { success: 1, guid }); validation failures acknowledge with the error message as a plain string.
load (client → server)
Emits the plain guid (no zip); the server merges it into the query passed to your beforeLoad hook. Acknowledged with the zipped document configuration plus the current revision:
socket.emit('load', guid, function(zippedConfig, rev) { ... });
If the document does not exist or beforeLoad denies access, the zipped payload decodes to { error: 1, message: 'Spreadsheet not found' }.
destroy (client → server)
Emits the plain guid. Destroys the document (adapter destroy), evicts the cache, emits forceRefresh to the room and disconnects its sockets.
forceRefresh (server → client)
{ args: [message] }. The server's instruction that local state can no longer be trusted. It is emitted in two cases: your persistence change handler reported an error ("There were an error trying to save the last operation."), or the document was destroyed ("This spreadsheet has been deleted"). The official client shows the message and reloads the page.
Presence operations
setBorder and resetBorders are presence, not data: they broadcast to the room but skip persistence and do not bump the revision (the broadcast carries the current one). On replay, the sender's user id becomes the border key, which is how each collaborator's selection is tracked. When a socket disconnects, the server broadcasts a resetBorders operation for each worksheet so peers clear that user's selection.
REST conventions
The REST API shares the same instance, hooks and revision counter as the socket path. See the route catalogue; the conventions:
- Authentication:
Authorization: Bearer <token>. An invitation hash may be appended after a comma:Bearer <token>,<invitation>. - Addressing:
/api/<guid>for the document,/api/<guid>/<n>/<action>for worksheetn(zero-based). A bareGET /api/<guid>returns the full configuration. - Write bodies: form-encoded (urlencoded or multipart), with nested structures in bracket notation, decoded server-side into arrays/objects:
data[0][x]=2&data[0][y]=4&data[0][value]=1310
- Write responses:
{ "message": "Done", "rev": 42 }. The revision is the same counter the sockets see: a REST write is broadcast to the document's socket room exactly like a socket write (skipped entirely when the room is empty). - Field limit: individual form fields up to 50 MB; oversized or malformed bodies return
400with a plain-text reason. - Errors:
403denied by your hooks,404for any request no registered module handles (unknown route, module, method, document or worksheet index),500with{ "message": ... }on internal failure.
Size and safety notes
- Socket payloads are capped by
maxHttpBufferSize(default 200 MB). - The server replays
methodstrings from the wire onto worksheet instances after yourbeforeChangegate. Validate methods there if your threat model includes hostile authenticated clients.