Documentation
Everything you need to add real-time synced state to your app.
Quick Start
Two commands. Zero cloning.
1. Start the server
Auto-creates a default namespace, prints your API key, and starts the server on http://localhost:3001. Data is stored in .better-state/ in your current directory.
2. Install the client SDK
Or for React projects:
3. Use it
Open the same page in two tabs — changes sync in real-time.
React Hooks
The @better-state/react package provides a useState-like API for synced state.
BetterStateProvider
Wrap your app with the provider to initialize the client:
useBetterState(key, initialValue)
Returns [value, set, update] — just like useState, but synced across every connected client.
useConnectionStatus()
Returns the current connection status: 'connecting' | 'connected' | 'disconnected'.
Client API Reference
createClient(url, options)
Creates a Better-State client and connects to the server.
client.state(key, initialValue)
Creates or retrieves a synced state object. If the key already exists on the server, the server's value replaces the initial value after connection.
state.get()
Returns the current local value synchronously.
state.set(value)
Replaces the state with a new value. Applies locally first (optimistic), then syncs to the server. Recommended for most use cases.
state.update(fn)
Applies a transformation function locally (optimistic), then syncs the resulting change to the server as a safe operation. In production apps, prefer explicit operations (like put, inc, and array ops) when you need precise conflict behavior.
state.subscribe(fn)
Fires on every state change (local and remote). Fires immediately with the current value. Returns an unsubscribe function.
client.status()
Returns 'connecting' | 'connected' | 'disconnected'.
client.onStatusChange(fn)
client.onError(fn)
Error codes include transport-level errors (CONNECT_ERROR, AUTH_ERROR) and mutation errors like bad_request, conflict, forbidden, internal_error.
client.disconnect()
Disconnects from the server and cleans up all listeners.
Server API
REST Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/health | Health check |
| GET | /api/v1/namespaces | List namespaces |
| POST | /api/v1/namespaces | Create namespace (returns API key) |
| GET | /api/v1/states | List state objects |
| GET | /api/v1/states/:key/history | Mutation history (paginated) |
Environment Variables
| Variable | Default | Description |
|---|---|---|
| PORT | 3001 | Server port |
| CORS_ORIGIN | * | Allowed origins |
| DATABASE_PATH | .better-state/state.db | SQLite database path |
| POLICY_MODE | permissive | Policy mode: permissive | strict |
| POLICY_PATH | (unset) | Path to policy JSON file (recommended for production) |
| MAX_KEY_LENGTH | 200 | Max key length for mutate requests |
| MAX_MUTATIONS_PER_BATCH | 100 | Max mutations per mutate request |
| MAX_WS_JSON_BYTES | 256kb | Max mutate payload size over WebSocket/Socket.IO |
| MAX_HTTP_JSON_BYTES | 256kb | Max JSON payload size for REST endpoints |
| LOG_LEVEL | info | Log level: debug | info | warn | error |
| LOG_PRETTY | false | Pretty-print logs when true/1 |
| STUDIO_PASSWORD | (unset) | Protect Studio endpoints when set |
Self-hosting
Protocol & Guarantees
Better-State synchronizes JSON documents per key using a durable event log and an authoritative snapshot (states.snapshot). Clients sync over WebSocket/Socket.IO.
For the exact WebSocket event names, payload shapes, ack/error contracts, and limits, see the full spec: Protocol Specification.
Guarantees
- Idempotency — each mutation must include a unique
id. Retrying the sameidwill not double-apply effects (server dedupes). - Ordering — server establishes authoritative ordering using
(server_ts, seq). - Optimistic Concurrency — clients send
baseVersion; server rejects conflicts withcode: 'conflict'. - Policy enforcement — server can reject ops based on key/op/path rules (strict or permissive mode).
Operations (Mutations)
Mutations are operation-based (no server-side code execution). Each mutation envelope includes an op describing a safe, deterministic change.
Mutation envelope
Common operation types
Paths use a conservative pointer-like format (e.g. /cards/abc/title).
Concurrency & Conflicts
Clients include baseVersion on mutate requests. If the server's current version differs, it rejects with code: 'conflict'.
Conflict error payload
Recommended client behavior
On conflict: resubscribe to receive the latest state:init/state:update, then retry pending mutations with the new version.
Policy (Authorization)
Policy is enforced on the server before persisting events. This prevents clients from bypassing product-level role checks via devtools or custom scripts.
Mode
POLICY_MODE=permissive(default): if no rule matches a key, mutations are allowed.POLICY_MODE=strict: if no rule matches a key, mutations are rejected (forbidden).
Policy file example
Limits & Safety
Better-State enforces global ingress limits to protect the server from accidental or abusive payloads. These limits apply regardless of policy mode.
MAX_KEY_LENGTH— rejects overly long keysMAX_MUTATIONS_PER_BATCH— rejects large mutation batchesMAX_WS_JSON_BYTES— rejects oversized WebSocket mutate payloadsMAX_HTTP_JSON_BYTES— caps REST JSON request size
Observability
The server emits structured logs and maintains lightweight in-memory metrics counters and timings.
Metrics endpoint (Studio)
If Studio is enabled, you can fetch a metrics snapshot:GET /api/v1/studio/metrics
Useful counters
How It Works
- Client applies locally first — UI updates instantly (optimistic)
- Mutation sent to server via WebSocket
- Server appends to event log and updates the authoritative snapshot
- Server broadcasts the authoritative state to all subscribed clients
- Clients replace their local value with the server's version
Conflict Resolution
Better-State uses optimistic concurrency via baseVersion. If the server detects a version mismatch, it rejects the mutation batch with code: 'conflict'.