Protocol Specification

This page documents the Better-State wire protocol used by the client SDK and server. It defines WebSocket events, payload shapes, acks, and error contracts.

Overview

Better-State synchronizes JSON documents per key within a namespace. Clients connect using Socket.IO and exchange a small set of events:

  • auth → authenticate into a namespace using an API key
  • subscribe → join rooms for keys and receive state:init
  • mutate → submit operation-based mutations (no code execution on server)
  • state:update → server broadcast of authoritative state + version

JSON values

Values are JSON-serializable (objects, arrays, strings, numbers, booleans, null). Non-JSON types should be avoided.

Paths

Many operations target a JSON pointer-like path (e.g. /cards/abc/title). Paths are conservative and slash-separated.

WebSocket Events

Event direction:

EventDirectionPurpose
authclient → serverAuthenticate using API key
auth:okserver → clientAuth success
auth:errorserver → clientAuth failure
subscribeclient → serverSubscribe to keys (rooms)
state:initserver → clientInitial snapshot per key
mutateclient → serverSubmit mutations for a key
mutate:ackserver → clientAck mutation ids + server version + dedupe info
mutate:errorserver → clientReject mutation request (bad_request/conflict/forbidden)
state:updateserver → clientBroadcast authoritative value + version

auth / auth:ok / auth:error

auth (client → server)

payload
1{
2 apiKey: string,
3 clientId: string
4}

apiKey maps to a namespace. clientId is a caller-provided identifier used for debugging/analytics.

auth:ok (server → client)

payload
1{ sessionId: string }

auth:error (server → client)

payload
1{ message: string }

subscribe / state:init

subscribe (client → server)

Subscribe to one or more keys. Each key may include an optional initialValue. If the key does not exist, the server creates it using the provided initial value (or null).

payload
1{
2 keys: Array<
3 string |
4 { key: string; initialValue?: unknown }
5 >
6}

state:init (server → client)

payload
1{
2 key: string,
3 value: unknown, // JSON value
4 version: number // integer >= 0
5}

The server emits one state:init per subscribed key.

state:update

When the server accepts mutations for a key, it broadcasts an authoritative update to all subscribed clients (including the sender).

payload
1{
2 key: string,
3 value: unknown, // JSON value
4 version: number
5}

Clients should treat version as authoritative and monotonically increasing per key.

mutate / mutate:ack / mutate:error

mutate (client → server)

Submit a batch of mutations for a key. The batch is processed atomically.

payload
1{
2 key: string,
3 baseVersion?: number,
4 mutations: Array<{
5 id: string,
6 clientTs: number,
7 op: JsonOp,
8 value?: unknown
9 }>
10}

baseVersion enables optimistic concurrency. If it is provided and does not match the server's current version, the server rejects with code: "conflict".

Each mutation must include a unique id. The server usesid for idempotency/deduplication.

JsonOp

Mutations are operation-based (no arbitrary code execution). Common operations:

JsonOp examples
1// Replace the entire document
2{ type: "set", value: anyJsonValue }
3 
4// Shallow merge object fields (object-only)
5{ type: "merge", value: { ... } }
6 
7// Delete at path
8{ type: "del", path: "/a/b" }
9 
10// Put value at path
11{ type: "put", path: "/a/b", value: anyJsonValue, createMissing?: boolean }
12 
13// Increment a numeric value at path
14{ type: "inc", path: "/counter", by?: number }
15 
16// Array helpers
17{ type: "arrayInsert", path: "/items", index?: number, value: anyJsonValue }
18{ type: "arrayRemove", path: "/items", index: number }
19{ type: "arrayMove", path: "/items", from: number, to: number }
20{ type: "arraySplice", path: "/items", index: number, deleteCount: number, items?: anyJsonValue[] }

mutate:ack (server → client)

Server acknowledges mutation ids so the client can clear its offline queue deterministically.

payload
1{
2 mutationIds: string[],
3 version: number,
4 insertedIds: string[],
5 duplicateIds: string[]
6}
  • mutationIds: echoes all ids from the request batch
  • insertedIds: ids newly inserted into the server log (applied)
  • duplicateIds: ids already present (no-op)

mutate:error (server → client)

Rejection payloads are structured. Minimal fields include code and message; other fields depend on the error type.

bad_request
1{
2 code: "bad_request",
3 message: string,
4 details?: object
5}
conflict
1{
2 code: "conflict",
3 message: "Version conflict",
4 key: string,
5 baseVersion: number,
6 serverVersion: number
7}
forbidden (policy)
1{
2 ok: false,
3 code: "forbidden",
4 message: "Policy violation",
5 key: string,
6 reason: "no_matching_rule" | "batch_too_large" | "op_not_allowed" | "path_denied" | "path_not_allowed",
7 opIndex?: number,
8 opType?: string,
9 path?: string,
10 appliedRule?: { name?: string; keyPrefix: string },
11 details?: object
12}

For conflicts, recommended client behavior is: resubscribe to fetch the latest version, then retry pending mutations with the updated base version.

Error Codes

CodeMeaningTypical action
bad_requestInvalid payload or violates hard limitsFix client request
conflictbaseVersion mismatchResubscribe and retry
forbiddenPolicy rejects operationChange policy or client behavior
internal_errorUnexpected server errorRetry / report / inspect logs

Guarantees

  1. Idempotency — mutation id is deduped by the server (exactly-once effect for at-least-once delivery).
  2. Authoritative version — server increments version for newly inserted events only.
  3. Ordering — server ordering is derived from (server_ts, seq).
  4. Safe mutations — server applies validated JSON operations; no client code execution.

Limits

The server enforces hard limits to protect availability. Configurable via environment variables:

VariableDefaultMeaning
MAX_KEY_LENGTH200Max key length for mutate requests
MAX_MUTATIONS_PER_BATCH100Max number of mutations per mutate
MAX_WS_JSON_BYTES256kbMax JSON payload size for mutate over WS
MAX_HTTP_JSON_BYTES256kbMax JSON payload size for REST endpoints