Skip to content

TypeScript SDK

The Relay TypeScript SDK. connectRelaySubscriber subscribes to track objects and connectRelayPublisher publishes them, through Relay, Mux’s Media over QUIC (MoQ) relay service. The SDK does not interpret payloads, so you need no player, media catalog, or Video.js.

  • Browser WebTransport in a secure context (HTTPS or localhost). This is the default transport. It needs WebTransport protocol negotiation, which the live tests verify against the Relay in Chromium and in Firefox 155 or later (Playwright’s builds). WebKit cannot open a WebTransport session to the Relay yet.
  • Node 24 or later: import from @caprid/relay/node, which connects over native QUIC through @caprid/relay-quic (Node). Other runtimes must pass a compatible createTransport factory.
  • Where UDP is blocked, or the browser has no usable WebTransport (Safari), pass @caprid/relay-websocket’s withWebSocketFallback() or createWebSocketTransport() as createTransport. The connection then runs QMux over WebSocket on TCP 443 (networks without UDP).
  • A short-lived Relay JWT authorized for the namespace you use, and the endpoint of any relay other than the hosted one. The SDK consumes tokens. It does not issue them.
npm install @caprid/relay

Add @caprid/relay-quic for Node (Node), @caprid/relay-websocket for networks without UDP, and @caprid/moq-media to publish or play media. To build the packages from a checkout of this repository instead, see Use the local packages in your application.

import { connectRelayPublisher, connectRelaySubscriber, readText } from '@caprid/relay';
const getToken = () => token; // a Relay JWT allowed to publish and subscribe under its root
const namespace = ['quickstart', 'unique-run']; // relative to the token's root
const publisher = await connectRelayPublisher({ namespace, getToken });
const ticker = publisher.registerTrack({ track: 'ticker' });
await publisher.announced;
const subscriber = await connectRelaySubscriber({ getToken });
let received: (text: string) => void;
const arrived = new Promise<string>((resolve) => { received = resolve; });
const subscription = subscriber.subscribe(
{ namespace, track: 'ticker' },
{ onObject: (object) => { if (object.status === 'normal') received(readText(object)); } },
);
await subscription.subscribed; // a fresh namespace can briefly reject this with error 16; retry it
await ticker.writeText('hello from @caprid/relay'); // waits for the subscriber's demand
console.log(await arrived);
subscriber.close();
await publisher.close();

With no url, each connection dials the hosted Relay, https://relay.mux.global, with the token’s root as the path, so namespaces are relative to that root. Pass url for another relay, region or room; roomEndpoint builds one (endpoint and credentials).

The guide’s full demo runs the complete version of this, with credential handling, the subscribe retry, a deadline, and cleanup on failure.

These connections never reconnect. For a live application, connectSupervisedPublisher and connectSupervisedSubscriber reconnect with backoff. They keep tracks registered and subscribed across connections and publisher restarts (stay connected).

@caprid/relay/node exports everything @caprid/relay does. Its connectRelayPublisher, connectRelaySubscriber, and both connectSupervised* functions connect over native QUIC through @caprid/relay-quic, Mux’s transport adapter for Node. Native QUIC means MOQT directly on QUIC, with ALPN moqt-22 and a fallback to moqt-20. Pass the same https:// endpoint you pass in a browser.

npm install @caprid/relay @caprid/relay-quic

@caprid/relay-quic is an optional peer dependency that only @caprid/relay/node needs. It lists a prebuilt native binary for each supported platform as an optional dependency, and your package manager installs the one that matches (its README). Without the package or the binary, a connect call fails with RelayError code TRANSPORT_UNAVAILABLE.

The code is the browser example’s, with a different import:

import { connectRelayPublisher, connectRelaySubscriber, readText } from '@caprid/relay/node';
const namespace = ['quickstart', 'unique-run'];
const getToken = () => process.env.RELAY_TOKEN;
const publisher = await connectRelayPublisher({ namespace, getToken });
const ticker = publisher.registerTrack({ track: 'ticker' });
await publisher.announced;
const subscriber = await connectRelaySubscriber({ getToken });
const subscription = subscriber.subscribe(
{ namespace, track: 'ticker' },
{ onObject: (object) => { if (object.status === 'normal') console.log(readText(object)); } },
);
await subscription.subscribed;
await ticker.writeText('hello from Node');

By default a connection trusts the platform’s root certificates. Two options change that, on either connect function:

  • ca adds extra trust anchors to the platform’s roots. Pass PEM as a string, as bytes, or as an array of either. Use it for a relay behind a private certificate authority.
  • serverCertificateHashes takes [{ algorithm: 'sha-256', value }], in the W3C WebTransport shape. The server’s certificate must match one hash, and the adapter does not validate its chain.
import { readFile } from 'node:fs/promises';
const subscriber = await connectRelaySubscriber({ url, getToken, ca: await readFile('relay-ca.pem') });

Connections without trust options share one adapter. Passing createTransport replaces the adapter entirely. Passing it together with ca or serverCertificateHashes throws a TypeError. To combine them, build the factory yourself with nativeQuicTransport({ ca }) from @caprid/relay-quic.

An open connection keeps the Node process alive from the start of the dial until the connection has closed. After that the adapter holds nothing, so a script exits by itself once it has closed its publisher and subscriber (TA-25, TA-26). A script that stops early without closing them keeps running.

@caprid/relay/node uses @caprid/relay-quic even where a global WebTransport exists. If Node gains a WebTransport that can negotiate protocols, the first connect emits a DeprecationWarning (code CAPRID_RELAY_NODE_NATIVE_WEBTRANSPORT) through process.emitWarning, once per process. At that point @caprid/relay connects on its own, and this entry point will be retired.

Deno support is best effort and untested. Deno can load the entry as npm:@caprid/relay/node and the addon through its npm compatibility layer. Deno’s own WebTransport cannot negotiate protocols, so plain @caprid/relay fails there with TRANSPORT_UNAVAILABLE.

Before 1.0, a minor version can break the API, and CHANGELOG.md says what changed. @caprid/relay, @caprid/moq-core, @caprid/moq-media, @caprid/moq-mediabunny, @caprid/relay-quic, and @caprid/relay-websocket always release together at the same version, so install matching versions. A release supports:

  • browsers with WebTransport, and Node 24 or later on the platforms @caprid/relay-quic ships binaries for (Linux x64 and arm64 with glibc or musl, macOS arm64 and x64, Windows x64).
  • MOQT draft 22 (moqt-22), with draft 20 (moqt-20) as an offered fallback, against moq-relay 0.15.4 or later.
  • TypeScript guide: the quickstart, subscription and publishing lifecycles, async consumption, and publishing and playing media.
  • API reference: run mise run ts:docs from a checkout to build it from the source into ts/docs-api/.
  • Errors and recovery: stable RelayError codes and phases, and how to recover without assuming delivery or safe replay.
  • Shared specification: the protocol pins, behavior contract, and test vectors every Relay SDK implements.