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 keysubscribe→ join rooms for keys and receivestate:initmutate→ 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:
| Event | Direction | Purpose |
|---|---|---|
| auth | client → server | Authenticate using API key |
| auth:ok | server → client | Auth success |
| auth:error | server → client | Auth failure |
| subscribe | client → server | Subscribe to keys (rooms) |
| state:init | server → client | Initial snapshot per key |
| mutate | client → server | Submit mutations for a key |
| mutate:ack | server → client | Ack mutation ids + server version + dedupe info |
| mutate:error | server → client | Reject mutation request (bad_request/conflict/forbidden) |
| state:update | server → client | Broadcast authoritative value + version |
auth / auth:ok / auth:error
auth (client → server)
apiKey maps to a namespace. clientId is a caller-provided identifier used for debugging/analytics.
auth:ok (server → client)
auth:error (server → client)
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).
state:init (server → client)
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).
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.
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:
mutate:ack (server → client)
Server acknowledges mutation ids so the client can clear its offline queue deterministically.
mutationIds: echoes all ids from the request batchinsertedIds: 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.
For conflicts, recommended client behavior is: resubscribe to fetch the latest version, then retry pending mutations with the updated base version.
Error Codes
| Code | Meaning | Typical action |
|---|---|---|
| bad_request | Invalid payload or violates hard limits | Fix client request |
| conflict | baseVersion mismatch | Resubscribe and retry |
| forbidden | Policy rejects operation | Change policy or client behavior |
| internal_error | Unexpected server error | Retry / report / inspect logs |
Guarantees
- Idempotency — mutation
idis deduped by the server (exactly-once effect for at-least-once delivery). - Authoritative version — server increments version for newly inserted events only.
- Ordering — server ordering is derived from
(server_ts, seq). - Safe mutations — server applies validated JSON operations; no client code execution.
Limits
The server enforces hard limits to protect availability. Configurable via environment variables:
| Variable | Default | Meaning |
|---|---|---|
| MAX_KEY_LENGTH | 200 | Max key length for mutate requests |
| MAX_MUTATIONS_PER_BATCH | 100 | Max number of mutations per mutate |
| MAX_WS_JSON_BYTES | 256kb | Max JSON payload size for mutate over WS |
| MAX_HTTP_JSON_BYTES | 256kb | Max JSON payload size for REST endpoints |