Define things once.
Use them boundlessly.
One secure connection. Two typed planes. Call functions across the network with typed actions; share live, server-owned state with realms — optimistic writes, instant everywhere. Both multiplex over a single self-healing socket, errors stay typed from throw to catch, and devtools come along for free.
// @nice-code/error — a typed error domain const err_user = defineNiceError({ domain: "err_user", schema: { not_found: err<{ userId: string }>({ httpStatusCode: 404 }) }, }); // @nice-code/action — input, output & the errors it throws const getUser = actionSchema() .input({ schema: v.object({ userId: v.string() }) }) .output({ schema: v.object({ id: v.string() }) }) .throws(err_user, ["not_found"]); // Call it anywhere — local, WebSocket, HTTP — fully typed const user = await act_user.action.getUser .request({ userId: "u_1" }).runToOutput(); // @nice-code/realm — or share it live: authoritative state, optimistic writes gameRealm.update((s) => { s.online[user.id] = user.name; });
Adopt what you need.
Start where it points.
The @nice-code suite is a set of libraries you pick up as you need them, not a framework you buy into whole. Find the shape closest to your project — each one is a complete, runnable round trip.
One module is the contract.
Every side imports it.
Every client–server app has a contract: where each side lives on the network, which calls exist, what shapes they take, which errors they can raise, what state is shared. Usually it's scattered — an API spec here, duplicated types there, error handling by convention. In nice-code the contract is one ordinary TypeScript module that every side imports directly: no schema compiler, no generated client, nothing to drift.
- 01 connection one secure link per peer
Every app holds exactly one connection to each peer it talks to — authenticated with the coordinates from the shared module (the handshake proves who's who), encrypted, and self-healing: a dropped socket redials and resumes on its own. Actions and realms are protocols multiplexed over that one socket. A realm-only app opens it with createWireClient and needs no actions at all; an actions app opens the same kind of link by connecting its channel.
// shared/runtimeCoordinates.ts — the network label, its own file, shared by all export const frontendCoord = RuntimeCoordinate.env("frontend"); // frontend.ts — a realm-only app: @nice-code/wire alone opens the connection const client = createWireClient({ identity: frontendCoord, peer: serverCoord, storage, transports: [{ carrier: wsCarrier(() => ({ url })) }], }); // …an actions app opens the same kind of link — see channel, below
frontend shared/runtimeCoordinates.ts → frontend.ts - 02 action a typed call
A single typed operation — a command, a query, or a server-to-client push. You declare its input, its output, and the errors it may throw. That one declaration is both the compile-time type and the runtime validator; there is no generated client to drift. Actions live in their own file — a domain grows.
getUser: actionSchema() .input({ schema: v.object({ userId: v.string() }) }) .output({ schema: v.object({ name: v.string() }) }) .throws(err_user, ["not_found"]),
shared shared/userActions.ts - 03 channel who calls what
A channel declares which actions may flow each way between two apps: what your frontend may call on the server, and what the server may push back. It imports the action domains from their own files — it lists them, it doesn't define them. Each actions app builds one ActionRuntime from its shared coordinate and connects the channel through it; your server accepts — and the one connection this opens carries realms too.
import { act_user } from "./userActions"; // its own file export const appChannel = defineChannel({ toAcceptor: [act_user], // frontend → server calls toConnector: [act_feed], // server → frontend pushes });
shared shared/channel.ts - 04 realm shared live state
Where actions are things that happen, a realm is state many parties watch — positions, presence, inventory, lobbies. The server owns the authoritative tree; every client gets a live projection it can write to optimistically, sliced by the realm's own visibility rules.
export const gameRealm = defineRealm({ id: "game_realm", state: { players: t.record(…) }, // the shared tree rules: (r) => [ … ], // who sees + writes what });
shared shared/gameRealm.ts - 05 avatar you, inside a realm
When a runtime client attaches to a realm it does so as an avatar — its identity inside that world. Rules read the avatar to decide what it may see and what it may change, and the authenticated connection is what proves the avatar is who it claims.
// each connected client attaches as an avatar avatars: { player: { persistentId: t.string() } }, // …a rule then reads the avatar to authorize a write .alter(({ avatar, params }) => avatar.persistentId === params.playerId),
shared shared/gameRealm.ts
The shared contract, assembled — a few small files, one import away
Your actions live in their own file and your realm in its own — a domain and a state tree both grow large, so both stand alone (your error domains sit in a shared file too). Two more small, independent files handle the network: runtimeCoordinates.ts names where each side lives, and a thin channel.ts lists what flows each way (the connection it opens carries the realm too). Every file here is shared — the frontend and the backend import it unchanged. The one piece each side builds for itself is its runtime, from a shared coordinate — and that's exactly what 02 and 03 do next.
// Actions get their own file — a domain grows; keep it self-contained import { err_user } from "./errors"; // A root domain is a namespace; a child domain groups related actions export const act_app = createActionRootDomain({ domain: "act_app" }); export const act_user = act_app.createChildDomain({ domain: "act_user", actions: { getUser: actionSchema() .input({ schema: v.object({ userId: v.string() }) }) .output({ schema: v.object({ name: v.string() }) }) .throws(err_user, ["not_found"]), // errors are part of the shape }, });
// State many parties watch — the tree, the avatars, the rules export const gameRealm = defineRealm({ id: "game_realm", avatars: { player: { persistentId: t.string() } }, state: { players: t.record(t.id("playerId"), t.object({ x: t.number(), y: t.number() })), }, rules: (r) => [ r.path("players.$playerId.{x,y}") .view(r.everyone) // everyone sees every player .alter(({ avatar, params }) => // only you may move yours avatar.persistentId === params.playerId), ], });
…and two small, independent files wire those definitions to the network — where each side lives, and what flows each way. Both imported, never redefined:
// Coordinates: the network label for each side. Defined ONCE, in their own // file, so the frontend, the server, and any worker agree on who's who. export const frontendCoord = RuntimeCoordinate.env("frontend"); export const serverCoord = RuntimeCoordinate.env("backend");
// The channel: which action domains flow each way. Imported by every side. import { act_user } from "./userActions"; // actions live in their own file import { act_feed } from "./feedActions"; // server → frontend pushes // The channel routes actions — the connection it opens carries realms too export const appChannel = defineChannel({ toAcceptor: [act_user], // frontend → server calls toConnector: [act_feed], // server → frontend pushes });
Call from one side.
Implement on the other.
Actions are the things that happen — commands, queries, server pushes. The client connects once, then calls actions like local functions; the server wraps the shared domain with real implementations and serves the channel. nice-code carries the transport, runs the validation, and keeps the types true on the far side.
// Both coordinates come straight from the shared module — no magic strings import { appChannel, act_user, frontendCoord, serverCoord } from "./shared"; // One runtime per app, built from the shared frontend coordinate const frontendRuntime = new ActionRuntime(frontendCoord); // Connect once to the server coordinate you're dialing connectChannel(frontendRuntime, appChannel, { peer: serverCoord, storage, transports: [{ carrier: httpCarrier(() => ({ url: apiUrl })) }], }); // ...then call from anywhere — output fully typed const user = await act_user.action.getUser .request({ userId: "u_1" }).runToOutput();
// The channel + the server's own coordinate — both from the shared module import { appChannel, act_user, serverCoord } from "./shared"; // The server is just another peer — one runtime, from the shared coordinate const serverRuntime = new ActionRuntime(serverCoord); // Implement the action, then serve the channel const userHandler = act_user.wrapAsLocalHandler({ getUser: async ({ userId }) => db.users.find(userId), }); const server = serveChannel(serverRuntime, appChannel, { storage, handlers: [userHandler], carriers: [httpAcceptorCarrier()], }); // server.fetch is a standard web handler — Bun · Deno · Workers run it directly export default { fetch: server.fetch };
One authoritative state tree,
every client live.
A realm is for state that many parties watch — presence, positions,
inventory, lobbies. The gameRealm you defined above is everything the client needs to
predict and everything the server needs to enforce: attach over your app's one connection — the
actions channel you just built, or a bare wire client with no actions at all — and you get
a live, optimistically-writable projection, hosted, persisted, and rule-checked on the other side.
Two planes, one connection. Or just the one plane — a realm-only app never imports actions.
// Attach over your app's one connection — the wire client from 01 (realm-only), // or an actions app's connectChannel connector. Same realm either way. const realm = connectRealm(gameRealm, { avatar: { type: "player" }, connection: realmConnection(client), // ← or realmConnection(connector) }); // Optimistic write — instant locally, confirmed (or rolled back) by the server realm.update((draft) => { draft.players[me].x = 412; }); // React — re-renders only when your slice changes const pos = useRealmValue(realm, (v) => v.players[me]); // Read-your-writes: after await, the store reflects the authoritative outcome await realm.intents.purchase({ purchaseId: newId(), itemId });
// Host it — on Cloudflare, persistence + hibernation come free const realms = serveRealmDurableObject(this.ctx, { realms: { game_realm: hostRealm(gameRealm, { avatarType: "player", // identity = the authenticated socket engine: { ctx: serverCtx, initialState: (info) => makeGenesis(info.instanceId), }, }), }, }); // A realm-only DO: wire serves the sockets — no actions, no channel const server = serveWireDurableObject(this.ctx, { identity: serverCoord.specify({ insId: this.ctx.id.toString() }), protocols: [realms.protocol], // ← the whole integration }); // Also serving actions? Register realms.protocol on action's // serveDurableObject instead — both planes share the same sockets.
Deployed and running,
right now.
Everything above is live on Cloudflare — real Durable Objects, real hibernation, real reconnects. Open one in two tabs (or two devices) and watch the same realm from both sides.
Eight packages,
one type-safe system.
The rest of the stack isn't add-ons — it's the shared substrate both planes are built from.
Declare a thing once (an error, a schema, an identity) and it stays consistent, validated, and typed
everywhere it travels: through an action call, into a realm rule, down to storage, and back out of a
catch block. Each package still stands perfectly well on its own.
Errors as data
Declare your error schema up front. Every id, every context field, every status — typed and reachable from any catch.
- Typed context: optional / required
- Multi-id errors in one object
- Parent/child domain hierarchies
- castNiceError · handleWith · matchFirst
Reactive state
Immer-backed store with fine-grained selectors, derived reactions, and patch streams. Tear-free React adapter.
- Mutate via Immer drafts
- Selector subscriptions, no provider
- Reactions derive state from state
- Patches + devtools timeline
Validation, shared
A Standard Schema validation error domain plus Hono middleware that throws a NiceError instead of a bare 400.
- err_validation domain
- niceSValidator drop-in
- niceCatchSValidation interceptor
- Works with Valibot, Zod, …
Storage + crypto
Typed storage adapters (browser, Durable Objects, memory) and WebCrypto helpers — Ed25519, X25519, AES-GCM.
- ITypedStorage over any backend
- Ed25519 sign / verify
- X25519 + AES-GCM shared keys
- ClientCryptoKeyLink
Built to ship,
documented to run.
When the prototype becomes the product, the answers are already written down — what the engine
bounds for you, what upgrades promise, and every error the stack can raise. And your AI pair
programmer gets the whole API as one file: point it at /llms.txt.