Better-State
A shared state primitive that makes realtime sync, optimistic updates, and offline support feel like one line of state — not a new architecture.
The thing that kept repeating across every product
Every time I built something collaborative, the same tax showed up:
- —a flurry of WebSocket handlers
- —state reconciliation logic scattered across the app
- —“offline later” as a lie we told ourselves
- —edge cases that only appeared when someone’s train went through a tunnel
And the worst part: the code wasn’t hard because the UI was complex. It was hard because the protocol was implicit—hidden in a thousand little decisions.
So I stopped trying to “add realtime” to apps.
I tried to build a primitive.
One that lets you write:
- —“this is state”
- —“this is how it changes”
- —“sync it everywhere”
- —“don’t break when I go offline”
…without shipping a new ecosystem or a full CRDT thesis.
Distributed state shouldn’t be a bespoke project. It should feel like a primitive.
The walls I had to design around
The bets I placed
Make the protocol explicit: event log → replay → broadcast.
Instead of treating state as “whatever the latest value is,” I treated it as the result of a sequence of mutations.
That unlocks three things:
- —History (what happened)
- —Replay (how we got here)
- —Deterministic recovery (how we resync after chaos)
// The API I wanted: simple, explicit, offline-ready.
const store = createStore({
key: "room-123",
initial: { count: 0 },
// Optimistic updates happen instantly
update: (state, action) => {
if (action.type === "INCREMENT") {
state.count += 1;
}
},
// Sync happens in the background
sync: "wss://api.better-state.dev"
});Local-first UI with authoritative convergence.
Clients apply mutations locally first (optimistic), then communicate the mutation.
The server becomes the authority that appends to the log, replays to compute the canonical state, and broadcasts back to everyone.
Design for the common case, protect the edge case.
Most apps don’t need a novel conflict-free data type to be useful.
They need a reliable “shared counter / todos / presence / polls / app flags” primitive that doesn’t implode under reconnect storms.
So I chose:
- —optimistic concurrency + resync
- —deterministic retry of pending mutations after snapshot
- —a small surface area that can be reasoned about
Make it self-hostable by default.
If shared state is infrastructure, you should be able to run it like infrastructure.
So Better-State ships as a server you can start with a single command and store data locally (SQLite).
Treat shared state as a protocol with an event log, not as “the latest value.”
Where it broke (and why it mattered)
The first versions mostly worked.
Which is a dangerous state in realtime systems.
The bug wasn’t a crash. It was worse: subtle divergence.
It would happen when:
- —a client went offline,
- —queued mutations,
- —reconnected quickly,
- —and sent a burst while also receiving server broadcasts.
In that overlap, I had a period where the client believed it had authoritative state, while the server was replaying a slightly different sequence. The result wasn’t immediately visible—but it showed up later as “why are these two tabs disagreeing?”
The fix wasn’t “more retries.” It was making the resync path a first-class event:
- —client reconnects
- —client requests/receives a server snapshot + cursor
- —client applies snapshot
- —client replays its queued mutations deterministically (in order)
- —server accepts/appends/broadcasts
- —client clears queue only when acknowledged
That failure changed how I think about realtime:
If your recovery path isn’t explicit, your system isn’t reliable. It’s lucky.
How it actually works
Better-State is intentionally boring in the right places.
It’s a small engine that does four jobs well:
Client
- —
state(key, initial)creates a state object - —
subscribe(cb)re-renders/reacts on changes - —
set(value)/update(fn)expresses mutations - —offline queue stores pending mutations (so refresh doesn’t lose intent)
Transport
- —WebSocket connection with auth (API key)
- —subscribe / mutate messages
- —reconnection with resync semantics
Server
- —authenticates API keys and namespaces
- —appends mutations to an event log
- —replays mutations to compute current state
- —broadcasts authoritative state to subscribers
Storage
- —SQLite persistence for durability
- —REST endpoints for health, namespaces, state listing, and history

What I’d tell myself before starting
Where this goes next
Better-State is designed to be a foundation: simple enough to adopt, structured enough to grow.
The roadmap is about expanding power without losing clarity:
- —Stronger conflict strategies for richer data shapes (beyond last-write-wins style state)
- —More inspection tooling (timeline views, mutation diffing, “why did this change?”)
- —Configurable policies (per-key access rules, rate limits, payload constraints)
- —Lightweight presence/rooms primitives built on the same engine
- —Production hardening: backups, migrations, and operational docs
But the north star stays unchanged:
Everything should still feel like one line of state.

A bored developer is a dangerous developer.