Mental Model
Realms are the state plane — one server-owned state tree that many clients watch and optimistically write, over your app's secure connection.
@nice-code/realm is a schema-defined, authoritative shared-state engine: many clients continuously observe (and optimistically write) one server-owned state tree, over a binary patch protocol riding your app’s secure connection. If the app also uses @nice-code/action, both share that one connection; a realm works just as well with no actions at all.
In one sentence
Section titled “In one sentence”Realm is the state plane; actions are the action plane — realm for state that many parties watch, actions for things that happen.
One-shot commands with results, reliability tiers, offline outboxes, progress streaming — those stay actions. Live positions, presence, inventory, lobby state, a shared cursor — that’s a realm.
| You want… | Use |
|---|---|
| ”What is the state of the world right now, kept live?“ | realm |
| ”Do this thing and tell me the result.” | action |
The loop
Section titled “The loop”The server owns the state. Clients hold a projection of it — the confirmed authoritative state with their own unconfirmed (optimistic) writes layered on top:
CLIENT SERVER ┌───────────────────────────┐ ┌──────────────────────────┐ │ projected view │ write frame │ authoritative state │ │ = confirmed + optimistic │ ───────────────────► │ rules checked, applied │ │ overlay │ │ in arrival order │ │ │ patches + ack │ │ │ rebase: drop the │ ◄─────────────────── │ broadcast, sliced per │ │ confirmed part of the │ │ client's view rules │ │ overlay, keep the rest │ │ │ └───────────────────────────┘ └──────────────────────────┘- A
realm.update()applies instantly to the local projection and is sent to the server. - The server validates it against the realm’s rules, applies it, and broadcasts the resulting patches — sliced per avatar, so state an avatar isn’t allowed to view never leaves the server.
- When the client’s own write comes back confirmed (or rejected), the optimistic overlay is rebased: confirmed parts drop out, rejected parts roll back, everything else stays.
Every write returns a settle handle — a lazy thenable that’s safe to ignore (fire-and-forget cursor moves produce zero unhandled rejections), with a read-your-writes guarantee: after await handle, the store already reflects the authoritative outcome.
One definition, both sides
Section titled “One definition, both sides”Everything derives from a single defineRealm block that both the client and the server import — the state schema, the avatar types, the rules, the intents. An avatar is the identity a connection presents to the realm — “who you are in this world” — and it’s what the rules judge: avatars names the kinds a realm admits (player, spectator, …), and every rule sees the acting avatar’s ref. From this one declaration the library derives the TypeScript types, the compact wire token map, a schema hash (so a server never silently reinterprets persisted state written under a different shape), and the client-side prediction bundle.
import { defineRealm, t } from "@nice-code/realm";
export const gameRealm = defineRealm({ id: "game_realm", avatars: { player: { persistentId: t.string() }, spectator: { persistentId: t.string() }, }, state: { config: t.object({ roundActive: t.boolean() }), players: t.record(t.id("playerId"), t.object({ x: t.number(), y: t.number() })), }, rules: (r) => [ r.path("config.*").view(r.everyone).alter(r.serverOnly), r.path("players.$playerId.{x,y}") .view(r.everyone) .alter(({ avatar, params }) => avatar.persistentId === params.playerId || myErr.fromId("not_yours")), ],});Because the same rules run on the client (prediction) and the server (authority), a write you aren’t allowed to make rejects locally, instantly — it never even reaches the wire. Only checks that need server-side facts (r.serverChecked) can differ, and that’s exactly the slice of optimism that can roll back.
One connection, two planes
Section titled “One connection, two planes”A realm attaches to your app’s secure connection — it registers as a frame protocol on the wire connection, and if you also use actions they share it: one socket, one handshake, one authenticated identity, two planes. Actions are optional; a realm-only app opens the same connection with createWireClient — @nice-code/wire and @nice-code/realm, nothing else.
- On the server: pass the realm host’s
protocoltoserveWireDurableObject(realm-only), or toserveDurableObjectwhen the DO also serves actions. - On the client:
connectRealm(realm, { connection: realmConnection(client) })whereclientis the connection returned bycreateWireClient(orconnectChannel, with actions).
The avatar identity defaults from the authenticated coordinate of that connection — the identity the secure handshake established — so realm rules like “only you may move your own player” are enforced against a cryptographically-bound identity, not a self-declared one.
The two write shapes
Section titled “The two write shapes”This distinction matters enough that it has its own page, but in short:
- Direct writes (
realm.update) are last-writer-wins with absolute-set semantics: “cursor is at 412”. High-frequency, coalesced, optimistic. - Intents are named, atomic, server-side mutations: “purchase item X”. Read-modify-write, counters, multi-path invariants — anything where composing the new value locally would race.
The pages that follow
Section titled “The pages that follow”- Defining a Realm — the schema builder, avatar types, and server context.
- Rules & Visibility — who can see what, who can write what.
- Writes & Intents — the write contract (read this one).
- Serving a Realm — the engine, the Cloudflare Durable Object host, persistence.
- Connecting & React — the client handle, liveness, the patch feed, hooks.
- Testing Realms — the deterministic in-memory world.