Typed actions.
Typed errors.
Reactive state.
Transport-agnostic, bi-directional actions with full TypeScript inference — call functions across the network, a worker, or a Durable Object as if they were local. Backed by typed errors, Immer state, and the crypto + storage primitives that tie it all together.
// @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 userDomain.action.getUser .request({ userId: "u_1" }).runToOutput(); // @nice-code/state — drop it into a reactive store userStore.update((s) => { s.user = user; });
Five small libraries,
one ergonomic surface.
@nice-code/action is the headline — typed calls that travel across any boundary. The rest
compose around it: errors declared with defineNiceError plug straight into action
.throws() clauses; secure channels use the crypto link from util; validation
failures surface as common-errors. Each package still stands on its own.
Actions that travel
Transport-agnostic, bi-directional calls with full inference. Local, over a WebSocket, over HTTP, or peer-to-peer.
- Channels declared once, by role
- Bi-directional push over one socket
- Deterministic results: expected vs unhandled
- WebSocket · HTTP · Durable Objects
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
Define once in shared code —
call it from either side.
// Implement the action, then serve the channel const handler = userDomain.wrapAsLocalHandler({ getUser: async ({ userId }) => db.users.find(userId), }); serveChannel(runtime, appChannel, { storage, handlers: [handler], carriers: [wsAcceptorCarrier({ /* host */ }), httpAcceptorCarrier()], });
// Connect once over carriers (WS preferred, HTTP fallback) connectChannel(clientRuntime, appChannel, { peer: serverCoord, storage, transports: [{ carrier: wsCarrier(wsUrl) }], }); // ...then call from anywhere — output fully typed const user = await userDomain.action.getUser .request({ userId: "u_1" }).runToOutput();