Introduction
What nice-code is — typed actions and live realms over one secure connection, plus the libraries they build on.
nice-code is a set of TypeScript libraries with one shared goal: your types stay correct everywhere your program goes — across the network, into a worker, down to storage, and inside a catch block. You write a type once and it follows your data the whole way.
The packages
Section titled “The packages”| Package | What it does |
|---|---|
@nice-code/wire | The secure connection everything rides — one self-healing, authenticated link that actions and realms multiplex over. |
@nice-code/action | Call a function on another machine as if it were local — fully typed. |
@nice-code/realm | Live, authoritative shared state — many clients watch and optimistically write one server-owned tree. |
@nice-code/error | Errors you can type, send over the network, and match on by name. |
@nice-code/state | A small state store (built on Immer) that only re-renders the parts that changed. |
@nice-code/common-errors | Ready-made validation errors + Hono middleware. |
@nice-code/util | Typed storage helpers and crypto helpers. |
Two planes, one connection. @nice-code/action (call functions) and @nice-code/realm (sync state) are the two things you build with — equal siblings, not a main package and an add-on. Both ride a single secure connection from @nice-code/wire, so an app that uses both multiplexes them over one socket. Use one plane or both: an actions app opens the connection with connectChannel, and a realm-only app opens it with createWireClient and needs no actions at all. The rest fit around them — actions describe their errors with error domains, secure connections use the crypto helper from util, and validation failures come back as common-errors.
How they fit together
Section titled “How they fit together”The example below shows the core idea. An action is a single shared definition — imported by both the client and the server — that describes one function’s input, its output, and the errors it can throw:
// Both the client and the server import this exact definition.export const act_user = act_app.createChildDomain({ domain: "act_user", actions: { getUser: actionSchema() .input({ schema: v.object({ userId: v.string() }) }) .output({ schema: v.object({ id: v.string(), name: v.string() }) }) .throws(err_user, ["not_found"]), // ← an error domain from @nice-code/error },});
// Call it from anywhere. The output is typed, and so are the errors you can catch.const user = await act_user.action.getUser .request({ userId: "u_1" }) .runToOutput();What nice-code is not
Section titled “What nice-code is not”- Not a validator. Bring your own — Valibot, Zod, or any Standard Schema library — to check data at the edges.
- Not an HTTP framework. Actions can travel over WebSocket, HTTP, in-memory, or WebRTC, and they plug into Hono, Workers, Node, Bun, and Durable Objects.
- Not magic. Each library is small, easy to read, and has very few dependencies.
Feeding the docs to an AI assistant
Section titled “Feeding the docs to an AI assistant”The whole documentation site is also published as a single plain-text file at /llms.txt — every page concatenated, frontmatter stripped, each section labelled with its route. Point a local AI coding assistant at it (or paste it into a chat) to give the model the library’s entire API surface as context in one shot, so its suggestions match how nice-code actually works. It’s regenerated on every docs build, so it never drifts from the pages you’re reading.
If you’re only using one package, there’s a smaller, focused file per module — point your agent at just the one you need: