The Connection
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
Section titled “Opening a connection”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 livecreateWireClient 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
Section titled “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:
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
Section titled “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 — 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.
The link lifecycle
Section titled “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: falseopts 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, orclient.addLinkEventListener).link_down,redial_scheduled(attempt + delay — a truthful “reconnecting in N s”),link_up(withdownForMs). 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).
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
Section titled “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. - Actions ride it as the request/response lane —
connectChanneliscreateWireClientwith an action channel bound on. See Serving & Connecting. - Both can share one connection: an app with actions and a realm opens a single
connectChannelsocket and binds the realm onto it.
The server twin
Section titled “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.