Skip to content

Quickstart

This takes you from a new Organization to one object published and received through a relay. You’ll use curl and jq to talk to Switchboard, and the SDK repository’s example to talk to the relay.

The examples use the Namespace acme. Use yours wherever you see it.

Sign in to the dashboard at app.caprid.com/app. Signing up creates an Organization with the Namespace you choose, and shows its first API key once. Keep it in your shell, and in your secret manager after that:

read -rs -p 'Switchboard API key: ' SB_API_KEY && export SB_API_KEY
curl https://app.caprid.com/v1/account -H "Authorization: Bearer $SB_API_KEY"

The API key manages your Organization. Relays never accept it, but it can mint tokens that they do, so treat it like a password and keep it on your servers. See API keys.

Client tokens are signed with one of your Namespace’s Signing keys. Create one:

curl -X POST https://app.caprid.com/v1/namespaces/acme/keys \
-H "Authorization: Bearer $SB_API_KEY" \
-d '{"label": "backend"}'

The response is a JWK with kid acme_backend. Its k is the secret your backend will sign tokens with in production; for now, Switchboard can sign them for you. See Signing keys and createSigningKey.

Ask for a token rooted at acme/quickstart that may both publish and subscribe beneath it. An empty prefix ("") means everything under the root:

export RELAY_TOKEN=$(curl -s -X POST https://app.caprid.com/v1/namespaces/acme/tokens \
-H "Authorization: Bearer $SB_API_KEY" \
-d '{"path": "quickstart", "publish": "", "subscribe": ""}' | jq -r .token)

Paths you send to the API are relative to the Namespace, so the token’s root claim is acme/quickstart. It expires in an hour. See mintClientToken, and check a token with verifyClientToken.

The first round trip runs the example in the SDK repository. You need Git and mise. The example reads RELAY_TOKEN from the environment, so it doesn’t prompt.

--room is your token’s root. The example connects to https://relay.mux.global/acme/quickstart, and the namespace you pass is relative to that root. Pick a fresh namespace for every run:

git clone https://github.com/caprids/relay-sdks.git && cd relay-sdks
mise install && mise run ts:install
mise run ts:example:mux-global -- \
--room acme/quickstart \
--namespace "run/$(date +%s)" --track ticker --count 1 --timeout 30

The task builds the native QUIC addon for Node from source, which needs Rust. mise brings the pinned toolchain. Installing from npm needs no Rust.

The demo publishes one object on track ticker and subscribes to it. The run succeeded when it prints:

finished published=1 received=1

If it doesn’t, see “If the run fails” in the TypeScript or Python guide.

The same round trip, with the SDK in your own project. roomEndpoint puts the token’s root in the URL of the hosted Relay, so the connection can’t reach outside it, and the namespace is relative to that root.

Install the SDK and its native QUIC transport for Node 24 or later:

npm install @caprid/relay @caprid/relay-quic
import { connectRelayPublisher, connectRelaySubscriber, readText, roomEndpoint } from '@caprid/relay/node';
const url = roomEndpoint(['acme', 'quickstart']);
const namespace = ['run', String(Date.now())];
const getToken = () => process.env.RELAY_TOKEN;
const publisher = await connectRelayPublisher({ url, namespace, getToken });
const ticker = publisher.registerTrack({ track: 'ticker' });
await publisher.announced;
const subscriber = await connectRelaySubscriber({ url, getToken });
const subscription = subscriber.subscribe(
{ namespace, track: 'ticker' },
{ onObject: (object) => { if (object.status === 'normal') console.log(readText(object)); } },
);
await subscription.subscribed; // a fresh namespace can briefly reject this with error 16; retry it
await ticker.writeText('hello from Node'); // waits for the subscriber's demand

In a browser, import from @caprid/relay instead, which connects over WebTransport and needs no @caprid/relay-quic. See Install.

These connections never reconnect. For a live application, use the supervised connections, which reconnect with backoff and keep tracks registered and subscribed (TypeScript, Python).

  • Sign tokens in your backend. Minting through the API is fine for testing. In production, sign them yourself with the Signing key, which saves a round trip per viewer and keeps working if Switchboard is unreachable: Sign your own.
  • Give each Client only what it needs. A viewer’s token should only subscribe, and only under its room: Client tokens.
  • Keep staging apart from prod. Prod and staging.
  • Handle failures. Errors have stable codes and phases: TypeScript, Python.