Protocol
Strata uses JSON text frames over WebSocket. Each frame has a version and a kind.
{ "v": 0, "kind": "ping" }The only supported wire version is 0. The server rejects another version.
Legacy frames have no request IDs or acknowledgements. Mutations can opt into
correlated outcomes with request_id.
The server checks the shared token during the WebSocket upgrade. Authentication is not a WebSocket frame.
Connection sequence
Section titled “Connection sequence”- Open
/socket/websocket?token=<STRATA_TOKEN>. - Wait for
welcome. - Send
joinfor each document that the application must open. - Merge the
snapshotfor each joined document. - Send
deltaandpresence_setframes only for joined documents. - Send
leavebefore the application closes a document.
The server can process a join before the application receives welcome.
Waiting for welcome makes connection state easier to understand.
Client to server frames
Section titled “Client to server frames”Required fields: v, kind, doc.
{ "v": 0, "kind": "join", "doc": "doc-1" }The server replies with snapshot. An unknown document has a null state:
{ "v": 0, "kind": "snapshot", "doc": "doc-1", "state": null }A join does not create stored document state.
Required fields: v, kind, doc.
{ "v": 0, "kind": "leave", "doc": "doc-1" }The server removes the socket from the document topic and removes its presence. The document remains in storage.
Required fields: v, kind, doc, state.
{ "v": 0, "kind": "delta", "doc": "doc-1", "state": { "type": "or_map", "val": {} }}state contains encoded ORMap state. In v0, it is not an operation-only
delta. The store validates and merges the state before the channel sends the
accepted merged state to other sockets on the document. It does not send the
frame back to the sender. Acceptance is in memory, not a crash-durable commit.
Invalid or incompatible ORMap state receives invalid_delta. A write after
deletion receives document_deleted. Neither rejection is broadcast.
storage_error reports a storage failure. outcome_unknown means the store
did not reply in time; the queued operation can still complete. A disconnect
also does not prove rejection. Rejoin to obtain authoritative state.
create
Section titled “create”Required fields: v, kind, doc, child, child_state, state.
{ "v": 0, "kind": "create", "doc": "parent-1", "child": "child-1", "child_state": { "type": "or_map", "val": {} }, "state": { "type": "or_map", "val": {} }}doc is the parent id. state is the new parent state. The server stores the
child, records its parent, and merges the parent state in one store operation.
Legacy success has no direct acknowledgement. Other sockets on the parent receive a
delta. Creating with a deleted child ID receives document_deleted; allocate
a new ID for a replacement.
delete
Section titled “delete”Required fields: v, kind, doc, child, state.
{ "v": 0, "kind": "delete", "doc": "parent-1", "child": "child-1", "state": { "type": "or_map", "val": {} }}The server verifies the recorded parent link. It removes the child and its
descendants. Each removed document topic receives deleted. Other sockets on
the parent receive a delta. A repeated delete succeeds when it names the
child's recorded deletion parent, including after restart if the deletion was
checkpointed. Tombstones have no expiry, and deleted IDs cannot be reused.
See Durability and Recovery for the flush boundary.
presence_set
Section titled “presence_set”Required fields: v, kind, doc, payload.
{ "v": 0, "kind": "presence_set", "doc": "doc-1", "payload": { "name": "Alice", "color": "#ff0000", "cursor": { "x": 10, "y": 20 } }}name and color are strings. cursor is application-defined JSON. The
server adds the socket session id to outbound presence records.
Required fields: v, kind.
{ "v": 0, "kind": "ping" }The server replies with pong.
Server to client frames
Section titled “Server to client frames”Correlated operation outcomes
Section titled “Correlated operation outcomes”Add a request_id containing 1 to 128 UTF-8 bytes to a delta, create, or
delete. IDs must be nonblank and unique within the logical client session.
Keep the same ID and exact payload when retrying. Legacy requests without
this field keep their existing response behavior.
{ "v": 0, "kind": "operation_outcome", "request_id": "session-unique-17", "doc": "parent-1", "outcome": "accepted_in_memory"}outcome has three values:
| Value | Meaning |
|---|---|
accepted_in_memory | The store accepted the mutation. It does not promise a successful checkpoint. |
rejected | This attempt failed validation or admission. The receipt includes code and message. |
unknown | The operation may still complete. The receipt includes message. A local timeout or disconnect has the same ambiguity. |
The server does not retain a request-ID deduplication cache. An ID correlates responses; it is not an exactly-once key. Replays undergo the current checks:
| Operation | Retry rule and retention |
|---|---|
| Delta | Resend the same encoded ORMap. CRDT merge makes duplicate state delivery idempotent. No operation-ID record is retained by the server. Deletion still takes precedence. |
| Delete | Resend the same parent ID, child ID, and parent state. The deleted child's retained parent identity permits repeat deletes. Tombstones have no expiry after checkpointing. |
| Create | Do not retry automatically. child_exists cannot prove that this request created the child. Rejoin and reconcile application state; do not translate a failed replay into failure of an earlier unknown attempt. |
Repeated safe operations may publish their accepted state again. An
outstanding duplicate beryl reply reference produces unknown, since the
first attempt can still complete. A response to a rejected retry describes
that attempt, not the history of an earlier submission. No crash-durable or
exactly-once receipt is offered. After a store-call timeout, late completion
can be visible in a later snapshot without another receipt.
Use strata_protocol/operation for request and receipt codecs. Use
strata_client/operation for bounded tracking, explicit safe retries, and
normal client events in one session. Hosts using only the legacy client API
must not request receipts that they cannot decode.
welcome
Section titled “welcome”{ "v": 0, "kind": "welcome", "session_id": "s-1", "server": "strata-0"}The session id identifies one socket connection. It changes after reconnect.
snapshot
Section titled “snapshot”{ "v": 0, "kind": "snapshot", "doc": "doc-1", "state": { "type": "or_map", "val": {} }}Merge the state into the local CRDT. Do not replace local state after a
reconnect. state is null when the server has no stored state for the id.
{ "v": 0, "kind": "delta", "doc": "doc-1", "state": { "type": "or_map", "val": {} }, "from": "s-1"}from is the sending socket id. Merge state into the local CRDT.
deleted
Section titled “deleted”{ "v": 0, "kind": "deleted", "doc": "child-1" }Close the local document and send leave. The server does not remove the
socket from the deleted topic.
presence_state
Section titled “presence_state”{ "v": 0, "kind": "presence_state", "doc": "doc-1", "presences": [ { "session_id": "s-1", "name": "Alice", "color": "#ff0000", "cursor": { "x": 10, "y": 20 } } ]}This frame gives the current presence list after a join.
presence_diff
Section titled “presence_diff”{ "v": 0, "kind": "presence_diff", "doc": "doc-1", "diff": { "joins": { "s-2": { "metas": [ { "name": "Bob", "color": "#0000ff", "cursor": { "x": 30, "y": 40 } } ] } }, "leaves": {} }}joins and leaves use the beryl presence format. Each object key is a
session id. strata_protocol/server.decode returns flat joins and leaves
lists and adds the key as session_id.
{ "v": 0, "kind": "error", "code": "invalid_delta", "message": "missing state field"}The server can send these codes:
| Code | Meaning |
|---|---|
invalid_topic | The document topic is invalid. |
unknown_event | The joined channel does not support the event. |
invalid_delta | The delta has no state field. |
invalid_presence | The presence frame has no payload field. |
invalid_create | A create field is missing, the parent is invalid for create, or a create ORMap state is invalid. The code stays invalid_create; the message identifies the parent or child and includes a decode summary when available. |
child_exists | The child id already exists. |
invalid_delete | A delete field is missing or the parent state is invalid. The code stays invalid_delete; the message identifies the parent and includes a decode summary when available. |
not_a_child | The live child does not belong to the specified parent. |
error | The channel runtime returned an error without a specific code. |
The HTTP upgrade returns status 403 for a missing or invalid token. It cannot
send a WebSocket error frame because the WebSocket does not open.
{ "v": 0, "kind": "pong" }Decode errors
Section titled “Decode errors”strata_protocol/error.DecodeError reports errors before a frame becomes a
typed value:
InvalidJsonmeans that the text is not valid JSON.InvalidFormatmeans that required fields have the wrong format.UnsupportedVersionmeans thatvis not0.UnknownKindmeans thatkindis not valid for that direction.
Decoders ignore additional object fields. They require all documented fields unless this page says that a field is optional.
Limits
Section titled “Limits”Legacy mutations have no success acknowledgement. Opt-in request_id
values provide the operation outcomes
described above.
Each snapshot contains the full encoded ORMap, including causal metadata,
in one WebSocket text frame. Deltas also use one frame. There are no
snapshot_begin, snapshot_chunk, or snapshot_end frames, no chunk
capability negotiation, and no snapshot_chunk_threshold setting.
Chunked snapshots remain post-1.0 work in
#5.
The server defaults to a 16 MiB encoded-document growth budget through
STRATA_MAX_DOCUMENT_BYTES. This limits accepted merged state, not the
complete wire frame or client memory use. The frame adds the document ID
and JSON envelope. The WebSocket stack or reverse proxy can impose lower
limits. Raising or disabling the budget does not enable chunking or raise
those limits. Existing oversized documents can still load from storage;
lowering the budget does not make their snapshots smaller.
After a disconnect, reconnect and rejoin to request a new full snapshot. Strata cannot resume a partial snapshot. Repeated reconnects cannot fix a frame that exceeds a transport limit. Use the snapshot sizing guidance to choose a budget that works with your deployed clients and proxy.
Document ids must be non-empty strings. The protocol does not restrict their length or character set. Avoid secrets and personal information in document ids because the admin page displays them.
Private admin frame
Section titled “Private admin frame”The admin SPA sends {"v":0,"kind":"admin_status"} to join the private
admin:status topic. The server sends an admin_status snapshot after the
join and at the configured interval. This frame is not part of the public
application protocol. It can change with the admin package.