Skip to content

Mental Model

Runtimes, peers, carriers, and channels — the four ideas behind nice-action.

@nice-code/action is a typed, transport-agnostic action system for calling functions across runtime boundaries (client/server, worker/worker, peer/peer) with full inference — including bi-directional calls where the acceptor pushes actions back over the same connection.

A runtime links to a peer over a carrier, and the routing between them is declared once as a channel.

Identity, auth, and encryption work the same regardless of carrier. Only two distinctions survive every carrier:

  • role — who dials (connector) vs who accepts and can push back (acceptor).
  • shapeduplex (a WebSocket / WebRTC channel: push-capable) vs exchange (HTTP: one request, one reply).
ConceptWhat it is
ActionDomainA named group of typed actions — your API surface.
ActionSchemaInput/output schema + declared error types for one action.
ActionRuntimeOne per runtime; identifies it and dispatches actions to handlers.
ChannelThe transport-agnostic routing contract + binary wire identity between two runtimes, declared by role (toAcceptor / toConnector).
CarrierHow bytes actually move: wsCarrier, httpCarrier, inMemoryCarrier, rtcCarrier (connector); wsAcceptorCarrier, httpAcceptorCarrier (acceptor).
TransportA carrier wrapped with a security policy. You don’t build these directly — connectChannel / serveChannel apply the policy for you.
RuntimeCoordinateIdentifies an environment (frontend, backend, worker…) — how actions are routed.

A client — a frontend, a backend, a worker — has a single ActionRuntime identifying it across every peer it talks to. Not one per feature, not one per backend. Register your local handlers on it, then connectChannel(...) once per peer (or serveChannel(...) to accept). This keeps one identity (and one crypto identity, for secure channels) per client and avoids routing ambiguity.

connectChannel (dial out) and serveChannel (accept) are what you reach for 95% of the time. They bind the channel, runtime, and crypto identity for you and desugar to lower-level handler/carrier/transport objects underneath.

A typical app is exactly three files:

  • shared.ts — imported by both ends: the domains, the channel.
  • server.ts — the acceptor: handlers + serveChannel.
  • client.ts — the connector: connectChannel + action calls.

The doc pages that follow are labelled by where each block runs.

Next: Defining actions →