# @nice-code/wire — documentation The connectivity backbone: dialing + transport fallback, the authenticated/encrypted handshake, secure sessions, reconnection, hibernation rehydrate, keepalive, and the frame multiplex several protocols share. This file concatenates only the @nice-code/wire pages of https://nicecode.io — point a local AI coding assistant at it when you're working with just this package. For the full set (every nice-code package and how they fit together) use /llms.txt. Generated from src/content/docs — do not edit by hand. --- # The Connection Source: /nice-wire/connection Description: createWireClient — the one secure connection substrate that action, realm, and devtools all ride: identity, transports and fallback, TOFU, and the self-healing link lifecycle. Everything in `@nice-code` that talks to another process talks over **one kind of connection**: a single secure socket with one handshake and one authenticated identity. `@nice-code/action` runs its request/response lane over it, `@nice-code/realm` rides it as a frame protocol, devtools observes it — but the connection itself is a `@nice-code/wire` concern, and `createWireClient` is how you open one directly. You reach for `createWireClient` when a realm (or any protocol) is the only thing on the socket — a realm-only app needs `@nice-code/wire` + `@nice-code/realm` and nothing else. When the app *also* has actions, `connectChannel` opens the same connection with an action channel bound on; both are the same substrate, so a realm's `realmConnection(...)` binds to either without knowing which. ## Opening a connection ```ts title="client.ts" import { createWireClient, wsCarrier } from "@nice-code/wire"; const client = createWireClient({ identity: myCoordinate, // who this client authenticates as peer: serverCoordinate, // the acceptor it dials storage, // durable backing for the crypto identity (see TOFU below) transports: [{ carrier: wsCarrier(() => ({ url })) }], }); await client.connect(); // dial + secure handshake — resolves when the link is live ``` `createWireClient` returns a **client handle**, not an open socket: it builds the transport chain and the crypto identity up front, and `connect()` performs the dial + handshake. It's idempotent (concurrent/repeat calls share one attempt) and rejects with `connect_exhausted` when the whole transport chain fails — a realm-only app awaits it; an app that keeps a protocol on the mux can also let the keep-alive ladder heal a first-attempt failure in the background. The four facts every connection needs: | Option | What it is | |---|---| | `identity` | This client's `RuntimeCoordinate` — its authenticated name to the peer. A realm defaults its avatar's `persistentId`/`instanceId` from it. | | `peer` | The acceptor's coordinate — who you're dialing. | | `storage` | A durable `StorageAdapter` backing the crypto keypair. Required for a secure connection (the default). | | `transports` | The carriers to try, in preference order (below). | ## Transports and the fallback chain A transport is a **carrier** plus whether it runs the secure handshake — `{ carrier, secure? }`. Carriers come from `@nice-code/wire`: `wsCarrier` (WebSocket), `rtcCarrier` (WebRTC), `inMemoryCarrier` (loopback, for tests and single-process). List several in preference order and the client prefers the first that's ready, falling through on failure: ```ts import { createWireClient, wsCarrier } from "@nice-code/wire"; createWireClient({ identity: myCoordinate, peer: serverCoordinate, storage, transports: [ { carrier: wsCarrier(() => ({ url: primaryUrl })) }, // preferred { carrier: wsCarrier(() => ({ url: fallbackUrl })) }, // used only if the first is exhausted ], }); ``` The carrier's `createRequest` closure runs per dial, so a **dynamic endpoint** (a realm that hops lobby → match) is just a closure that reads the current target: `wsCarrier(() => ({ url: urlFor(activeMatchId) }))`. Returning `null` from it means "no valid endpoint right now" — the keep-alive ladder parks instead of dialing garbage, so tearing the target down needs no careful ordering. A `createWireClient` connection needs a **duplex** carrier — a realm (or any mux protocol) is push-driven, and an exchange (request/reply, e.g. HTTP) carrier can't push. Exchange carriers are an action-lane concern; use them through `connectChannel`. ## Identity, storage, and security A secure connection authenticates with a crypto identity your `storage` holds, and the peer pins it on first contact (trust-on-first-use). The one rule to internalize *here*: **`storage` must be durable** — a memory/session store regenerates the keypair each reload, so every load after the first is rejected `identity_pin_mismatch`, forever. Coordinates, the crypto handshake, the pinning rules, and how to recover a mismatch are all on **[Identity & Trust](/nice-wire/identity/)** — the page that owns them. A **per-session** identity is fine as long as the id is minted fresh too — declare it with `ephemeralIdentity: true` to acknowledge the pairing and silence the durability warning. Pick `authenticated` (the default) or `encrypted` with `securityLevel`; a realm reads the negotiated level off the connection and refuses to attach below its own floor. See **[Security Levels](/nice-wire/security/)**. ## The link lifecycle Once a protocol rides the connection, the link **owns its own reconnection** — you don't re-dial on a drop: - **Keep-alive.** An unexpected drop auto-redials with exponential backoff + jitter (≈1 s → 30 s). Default: on when the mux carries a protocol (a realm), off for a bare/plain link. `keepLinkAlive: false` opts out. - **Half-open detection** (`linkKeepalive`). After an idle window the link sends a wire ping; no answer closes it locally, converting a silently-dead socket into the ordinary redial. - **Link events** (`onLinkEvent`, or `client.addLinkEventListener`). `link_down`, `redial_scheduled` (attempt + delay — a truthful "reconnecting in N s"), `link_up` (with `downForMs`). A clean heal is silent at a realm's sync layer; this is where transport churn is visible. - **Teardown.** `client.releaseLink()` stops the keep-alive and drops the link (reusable after); `client.dispose()` is the permanent stop. `client.reconnectLink()` forces an immediate re-dial (bind it to a "retry now" button). ```ts const client = createWireClient({ identity: myCoordinate, peer: serverCoordinate, storage, transports: [{ carrier: wsCarrier(() => ({ url })) }], keepLinkAlive: true, onLinkEvent: (event) => metrics.count(`link.${event.type}`), }); ``` ## One substrate, three planes The reason the connection is its own layer: **action, realm, and devtools are all protocols over the one socket**, so the resilience story — handshake, secure session, keep-alive, half-open detection, link events — is written once and every plane inherits it. - **Realm** rides the connection as a frame protocol — `connectRealm(realm, { connection: realmConnection(client) })`. See [Connecting & React](/nice-realm/connecting/). - **Actions** ride it as the request/response lane — `connectChannel` *is* `createWireClient` with an action channel bound on. See [Serving & Connecting](/nice-action/serving-connecting/). - Both can share one connection: an app with actions *and* a realm opens a single `connectChannel` socket and binds the realm onto it. ## The server twin The accepting end is the same substrate: on Cloudflare, **`serveWireDurableObject`** (from `@nice-code/wire/platform/cloudflare`) serves these connections from a Durable Object — the handshake, the DO-storage crypto identity + TOFU pins, hibernation rehydrate, and keepalive folded into one call, with protocols (a realm) registered on it directly. A realm-only DO imports wire + realm and nothing else, mirroring the client exactly. See [Serving a Realm](/nice-realm/serving/). --- # Identity & Trust Source: /nice-wire/identity Description: RuntimeCoordinate names each side of a connection; a crypto identity over durable storage proves it; trust-on-first-use pins the key. The one identity model both planes gate on. Every secure connection has two ends, and each end has a **name** and a **proof**. The name is a `RuntimeCoordinate`; the proof is a crypto identity backed by your `storage`; and **trust-on-first-use** (TOFU) is what ties a name to a key so the second connection can tell it's really the same peer. This is the one identity model the whole stack rests on — action routing, realm avatars, and devtools admission all read it. ## Coordinates — the network label A `RuntimeCoordinate` labels a runtime environment and routes traffic to the right side. It's exported from `@nice-code/wire` (and re-exported by `@nice-code/action`), and it lives in your shared module so both ends reference the exact same value: ```ts RuntimeCoordinate.env("backend"); // a named environment RuntimeCoordinate.env("backend").specify({ perId: "worker-1" }); // env + a per-instance id RuntimeCoordinate.env("backend").withPersistentId(id.toString()); // env + a persistent instance id RuntimeCoordinate.unknown; // unspecified ``` The `identity` you pass to `createWireClient` (or that an `ActionRuntime` is built from) is a coordinate; the `peer` you dial is the other side's coordinate. A realm defaults its avatar's `persistentId` / `instanceId` from the connection's authenticated coordinate — so "who is this avatar?" is answered by the same handshake that authenticated the socket. ## The crypto identity lives in your storage A secure connection authenticates with an Ed25519 keypair — a `ClientCryptoKeyLink` that `createWireClient` builds over the `storage` you give it. That storage holds this runtime's signing keys **and** the verify keys of peers it has met. It's why a secure connection needs `storage` and a fully `none` connection doesn't. The handshake runs once, before any protocol frame: each side sends its coordinate and public key, then proves it holds the matching private key by signing a fresh, single-use challenge that binds both nonces, both identities, the wire-dictionary version, the security level, and every key exchanged. A replayed or tampered handshake produces a different challenge and fails. After that one exchange, every message rides inside the already-authenticated session — no per-message crypto. (Which level of protection that session gives — identity only, or identity + encrypted frames — is [Security Levels](/nice-wire/security/).) ## Trust-on-first-use (TOFU) The first time a peer sees a given identity — keyed by the coordinate's `envId` + its **persistent id** (falling back to the per-boot instance id when none is set) — it **pins** that identity's verify key. A later handshake presenting a *different* key for the *same* identity is rejected with `identity_pin_mismatch`. That is what stops a third party who merely knows your id from impersonating you: they can't produce the pinned key's signature. `withPersistentId` is therefore a **trust decision, not just a routing name** — it tells the peer "remember my key under this id, forever." ### Your storage must be durable — the mistake that bites The verify key lives in `storage`. If that store is memory- or session-backed, the keypair regenerates on every reload while the pinned id stays the same — so the **first** load works and **every load after it is rejected**, permanently, for that identity. In a browser use `createWebLocalStorageAdapter`; on React Native back it with AsyncStorage (see [React Native / Expo](/getting-started/react-native/)); memory adapters are for tests. `createWireClient` / `connectChannel` warn at construction when they see a persistent id paired with a known-ephemeral store. A **per-session** identity is perfectly fine — as long as the id is minted fresh too (a startup `crypto.randomUUID()` as the coordinate's `perId`, with memory storage). Declare it with `ephemeralIdentity: true` to acknowledge the pairing and silence the warning. ### Recovering from a pin mismatch A pin mismatch is **permanent** for that identity+key pair — the keep-alive redial ladder *parks* on it rather than retrying a handshake that can never succeed. Two things that do **not** recover it, and two that do: - **Does not work:** clearing only the client's *crypto* storage. A fresh key under the same persistent id is exactly what the pin rejects. - **Works:** connect under a **new persistent id** (a fresh identity), *or* remove the peer-side pin via the verify-key resolver's storage. A rejected handshake surfaces where `connect()` (or the first dispatch) rejects — **don't fire-and-forget that promise.** The mismatch is typed, so an app can branch on it: ```ts import { err_wire_connect } from "@nice-code/wire"; try { await client.connect(); } catch (e) { if (err_wire_connect.isExact(e) && e.hasId("identity_pin_mismatch")) { // Permanent for this identity+key pair — recover under a new persistent id, // or clear the peer-side pin. Retrying the same pair never succeeds. } } ``` ## On Cloudflare: persist the pins A Durable Object host keeps its identity and its trusted-peer pins in DO storage (the default in `serveWireDurableObject`). A pin that lived only in memory would re-TOFU on every hibernation wake and defeat the protection — so on Cloudflare, persistence isn't optional. See [Serving a Realm](/nice-realm/serving/). For high-cardinality peers, call `createStorageTofuVerifyKeyResolver(storage, { layout: "per_identity" })`. The legacy/default `"document"` layout stores one `pins` map and rewrites it on each first contact; `"per_identity"` stores one domain-separated SHA-256-addressed record per identity, so unrelated pins do not contend or amplify writes. First contact for the same identity is serialized inside the resolver. The store must still be durable, and an application should authorize/cap which identities may reach TOFU when the peer population is attacker-controlled. Switching an existing host from `"document"` to `"per_identity"` is safe: on each identity's next contact its legacy pin is honored and migrated to a per-identity record, and the legacy `pins` document is left in place so rolling back is also safe. The reverse switch is not migrated — per-identity records are invisible to the `"document"` layout. ## What rests on this TOFU is not certificate-authority PKI: it's strong against an outsider who never held the key, but if first contact happens over a hostile network, the pin is of whoever answered. That matters most for a **realm**, whose entire authority model — every `alter` rule — rests on the avatar identity this pin establishes. See [Realm Security](/nice-realm/security/) for the realm-layer consequences, and the [Error Reference](/production/errors/) for branching on `identity_pin_mismatch` and its neighbours. --- # Security Levels Source: /nice-wire/security Description: From self-asserted identity to fully encrypted frames — one level, picked where the connection is opened, used by both ends. The same channel works at every level. Security is a **connection** property, chosen once where you open the link — `createWireClient` or `connectChannel` — and used by both ends. You don't touch your channel, your realm, your handlers, or your calls: one `ESecurityLevel`, and everything riding the socket inherits it. There are three levels: - **`none`** — each side just claims who it is; no verification. Fastest; fine for local dev or a trusted network. - **`authenticated`** (the default) — both sides prove who they are during a handshake (sign/verify, plus pinning each other's key the first time they meet). Messages travel in the clear. - **`encrypted`** — everything `authenticated` does, **plus** every message is sealed (AES-GCM) with a key worked out during the handshake. ```ts createWireClient({ identity, peer, storage, securityLevel: ESecurityLevel.encrypted, transports }); // …the identical option on connectChannel / serveChannel / serveWireDurableObject. ``` ## Why `authenticated` is safe even in the clear A natural worry: if `authenticated` messages travel in the clear, what stops someone spoofing them? Identity is proven **once, up front, in the handshake** — not per message. The client signs a fresh, single-use challenge with its private key; the server verifies against the key it pinned for that client on first contact (trust-on-first-use). Every later message rides inside that already-proven session, so it's cheap (no per-message crypto) yet unspoofable by an outsider who never held the key. The full handshake mechanics, the crypto identity your `storage` holds, and the TOFU pinning rules (including the durable-storage requirement and how to recover an `identity_pin_mismatch`) live on **[Identity & Trust](/nice-wire/identity/)** — the one page that owns them. What `authenticated` deliberately does *not* give you is confidentiality: payloads are readable on the wire (you lean on TLS for transport encryption, or step up to `encrypted` to seal each frame). ## How the level gets chosen The dialing side picks the level it wants. By default the listening side **accepts any of the three, per connection** — so one endpoint can serve all three. If the server persists what it learns about a connection (a carrier that supports sleep/wake), an `authenticated` or `encrypted` connection can pick up again after the server hibernates without redoing the handshake. A **realm** reads the negotiated level off the connection and refuses to attach below its own required floor — see [Realm Security](/nice-realm/security/). ## One channel works at every level The same secure `{ carrier: wsCarrier(...) }` transport works at any level. And `httpCarrier` runs that *same* secure session over plain HTTP (handshake → token → encrypted messages) — matching each reply to its request comes for free, since that's how an HTTP request/response already works. To offer a secure WebSocket with a plain HTTP fallback, give the listening side an `httpAcceptorCarrier({ secure: false })`: ```ts title="client.ts" connectChannel(clientRuntime, appChannel, { peer: serverCoord, storage, securityLevel: ESecurityLevel.encrypted, transports: [ { carrier: wsCarrier(() => ({ url: wsUrl })) }, // secure (default) { carrier: httpCarrier(() => ({ url: httpUrl })), secure: false }, // plain fallback ], }); ``` ## Secure HTTP needs no shared memory between requests Secure HTTP is **stateless**: the handshake and session details are carried inside sealed tokens (sealed with the server's own crypto identity), so any server instance can handle any request. You don't need a Durable Object just to keep the two POSTs of a handshake on the same instance: ```ts title="server.ts" const server = serveChannel(runtime, appChannel, { storage, // only stores the server's crypto identity — never the sessions carriers: [httpAcceptorCarrier()], handlers: [appHandler], }); // app.post("/action", (req) => server.fetch(req)) ``` If your storage is **eventually consistent** (like Cloudflare KV, where a fresh write isn't always readable immediately), create the identity once up front so a momentary read-miss can't accidentally mint a second identity: ```ts import { ClientCryptoKeyLink } from "@nice-code/util"; const link = new ClientCryptoKeyLink({ storageAdapter: storage, identityMode: "required" }); await link.provisionIdentity(); // once, out-of-band (a deploy step / first boot) serveChannel(runtime, appChannel, { storage, link, carriers: [httpAcceptorCarrier()], handlers: [appHandler] }); ```