Introduction
What nice-code is, in plain terms. 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. That covers the network, a worker, storage, and the inside of 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 else uses. One authenticated link that reconnects itself, shared by actions and realms. |
| @nice-code/action | Call a function on another machine as if it were local — fully typed. |
| @nice-code/realm | Live shared state. The server owns one state tree; every client watches it and writes to it optimistically. |
| @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. |
| @nice-code/process | Supervise a child process: spawn, readiness, restart backoff, log capture. |
| @nice-code/commander | Your dev environment as one typed config, with a web UI. |
| @nice-code/devtools | A dev-only window onto every action, realm, store and wire frame. Inert in production. |
You build with two of these. @nice-code/action calls functions, and @nice-code/realm keeps state in sync. The docs call them the two planes, and they are equals: neither is an add-on to the other. Both use a single secure connection from @nice-code/wire, so an app that uses both shares one socket between them. You can use either one alone. An actions app opens the connection with connectChannel, and a realm-only app opens it with createWireClient and needs no actions at all. The other packages 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 one shared definition that both the client and the server import. It describes a 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 to check incoming data: Valibot, Zod, or any Standard Schema library.
- 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. It holds every page joined together, with frontmatter stripped and each section labelled with its route. Point an AI coding assistant at it, or paste it into a chat, and the model has the library’s whole API as context, so its suggestions match how nice-code actually works. The file is regenerated on every docs build, so it never drifts from the pages you’re reading.
If you only use one package, there’s a smaller file for each module. Point your assistant at the one you need: