Skip to content
Strata is pre-1.0. APIs, protocol, and storage format may change.

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.

  1. Open /socket/websocket?token=<STRATA_TOKEN>.
  2. Wait for welcome.
  3. Send join for each document that the application must open.
  4. Merge the snapshot for each joined document.
  5. Send delta and presence_set frames only for joined documents.
  6. Send leave before 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.

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.

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.

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.

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.

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:

ValueMeaning
accepted_in_memoryThe store accepted the mutation. It does not promise a successful checkpoint.
rejectedThis attempt failed validation or admission. The receipt includes code and message.
unknownThe 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:

OperationRetry rule and retention
DeltaResend the same encoded ORMap. CRDT merge makes duplicate state delivery idempotent. No operation-ID record is retained by the server. Deletion still takes precedence.
DeleteResend 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.
CreateDo 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.

{
"v": 0,
"kind": "welcome",
"session_id": "s-1",
"server": "strata-0"
}

The session id identifies one socket connection. It changes after reconnect.

{
"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.

{ "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.

{
"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.

{
"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:

CodeMeaning
invalid_topicThe document topic is invalid.
unknown_eventThe joined channel does not support the event.
invalid_deltaThe delta has no state field.
invalid_presenceThe presence frame has no payload field.
invalid_createA 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_existsThe child id already exists.
invalid_deleteA 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_childThe live child does not belong to the specified parent.
errorThe 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" }

strata_protocol/error.DecodeError reports errors before a frame becomes a typed value:

  • InvalidJson means that the text is not valid JSON.
  • InvalidFormat means that required fields have the wrong format.
  • UnsupportedVersion means that v is not 0.
  • UnknownKind means that kind is not valid for that direction.

Decoders ignore additional object fields. They require all documented fields unless this page says that a field is optional.

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.

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.