Mental Model
A realm is live shared state owned by the server. Many clients watch it and can change it instantly, over your app's secure connection.
A realm is shared state that stays live for everyone. The server owns it; every connected client sees it update in real time, and can change it without waiting for a round trip. @nice-code/realm defines that state with a schema and syncs it as binary patches over your app’s secure connection. An app that also uses @nice-code/action shares that one connection; a realm works just as well with no actions at all.
Realm or action?
Section titled “Realm or action?”A realm (the state plane) holds state that many parties watch; actions (the action plane) are for things that happen.
One-shot commands with results, reliability tiers, offline outboxes and progress streaming stay actions. Live positions, presence, inventory, lobby state or a shared cursor belong in 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”Each client holds a projection of the server’s state: the confirmed state, with the client’s 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 to the local projection instantly and is sent to the server. - The server checks it against the realm’s rules, applies it, and broadcasts the patches. Broadcasts are sliced per avatar (a connection’s identity), so state an avatar may not view never leaves the server.
- When your own write comes back confirmed or rejected, the optimistic overlay is rebased: confirmed parts drop out, rejected parts roll back, and everything else stays.
Every write returns a settle handle, a lazy thenable that is safe to ignore: fire-and-forget cursor moves produce zero unhandled rejections. You also get read-your-writes: after await handle, the store already reflects the authoritative outcome.
One definition, both sides
Section titled “One definition, both sides”Everything comes from one defineRealm block that both the client and the server import: the state schema, the avatar types, the rules and the intents. An avatar is the identity a connection presents to the realm, “who you are in this world”, and 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 encoding, client-side prediction and a schema hash, so a server never silently reinterprets persisted state written under a different shape.
import { defineRealm, t } from "@nice-code/realm";import { err_game } from "./errors"; // a typed error domain
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 || err_game.fromId("not_yours")), ],});The same rules run on the client as prediction and on the server as authority, so a write you aren’t allowed to make is rejected locally and instantly, before it’s sent. Only checks that need server-side facts (r.serverChecked) can differ, and those are the only writes that can roll back.
One connection, several protocols
Section titled “One connection, several protocols”A realm attaches to your app’s secure connection as a frame protocol. If you also use actions or remote devtools, they share it: one socket, one handshake and one authenticated identity. Actions are optional: a realm-only app opens the same connection with createWireClient, using only @nice-code/wire and @nice-code/realm.
- 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 connection’s authenticated coordinate, the identity the secure handshake established. A rule like “only you may move your own player” is therefore enforced against a cryptographically bound identity, not a self-declared one.
The two write shapes
Section titled “The two write shapes”This distinction has its own page. In short:
- Direct writes (
realm.update) have absolute-set semantics: you send the new value (“cursor is at 412”) and the last writer wins. They are optimistic, and frequent writes are coalesced into the latest value. - Intents are named, atomic mutations that run on the server, such as “purchase item X”. Use them for read-modify-write, counters and 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.