# Authentication

> Authenticate your server, your app, and your end users with API keys, relay tokens, and JWTs.

You authenticate to FirstByte at two levels. Your **server**
authenticates with a long-lived API key. Each **end user or session**
connects with a short-lived token that you mint from that key. This
keeps powerful credentials on your backend. Clients get only
narrowly-scoped access.

Two auth surfaces cover the two kinds of traffic:

- **Control plane** (control-plane API tRPC: voice control, streams CRUD,
  agents, webhooks, analytics). This uses long-lived **API keys** scoped
  to an org and a set of capabilities.
- **Data plane** (MoQT publish / subscribe, audio bridge, robotics topics,
  games rooms, data pub/sub). This uses short-lived **relay tokens**
  scoped to a set of namespaces. The relay's namespace_auth hook checks
  each token on every publish or subscribe.

The two are linked. A client uses its API key to mint relay tokens
scoped to exactly the namespaces that it needs.

## Authenticate your server: API keys

Give your backend an API key so that it can call the control plane.
Create one in the dashboard or with
`streams.apiKeys.create({ orgId, label, scopes })`. The API returns the
key string **once**, at creation. The control-plane API stores only the
hash.

```bash
export CLUTCHCALL_API_KEY=tqs_live_abc123…
```

Every control-plane API call (voice originate, streams create, webhook
list, analytics, agent attach) authenticates with this key over HTTPS.
The control-plane API rejects a key that does not have the correct
capability scope for the called procedure.

## Authenticate a session: relay tokens

To let a client publish or subscribe on the data plane, mint a relay
token for it. Do not share your API key. The relay never sees the API
key. The control-plane API mints a short-lived **relay token**. The
token is a signed JWT that carries namespace claims. The client presents
the token during the MoQT handshake. The relay's `namespace_auth` hook
checks the JWT signature and extracts the namespace scope. It then gates
every subsequent publish / subscribe against that scope.

A typical relay token looks like:

```json
{
  "alg": "EdDSA",
  "kid": "rk_live_abc…",
  "iss": "clutchcall-bff",
  "sub": "tenant_abc",
  "ns":  ["voice/sid_xyz/*", "playback/li_xyz"],
  "exp": 1733900000
}
```

| Claim | Meaning                                                              |
| ----- | -------------------------------------------------------------------- |
| `kid` | Signing-key id. The relay fetches the matching public key from Redis. |
| `iss` | The control-plane API that minted the token.                         |
| `sub` | Tenant id the relay stamps on the session.                           |
| `ns`  | Namespace patterns this token may publish or subscribe to.           |
| `exp` | Expiry (typically 1h for client tokens, longer for service-to-service). |

Every modality client knows which namespaces it needs. It asks the
control-plane API for a token with that scope. You do not construct
these JWTs by hand. When you give the SDK a control-plane API key, its
modality clients call the control-plane API's token-mint procedures for
you.

## Authenticate an end user in the browser

In the browser, you cannot ship a long-lived API key. Instead, mint a
**short-lived relay token** on your server for each browser session.
Pass the token to the SPA:

```ts
// server (your control-plane API)
const token = await streams.signingKeys.mintPlaybackToken({
  inputId: "li_xyz", scopes: ["playback/li_xyz"], ttlSeconds: 3600,
});

// browser
const viewer = await BroadcastViewer.open(signedUrl);
```

For voice in a browser:

```ts
// server
const token = await voice.audioBridge.mintBrowserToken({
  callSid: "sid_xyz", ttlSeconds: 1800,
});
// browser
const v = new Voice({ baseUrl: controlPlaneApi, browserToken: token });
```

`browserToken` avoids the API-key-on-client antipattern. The SDK
presents the token directly to the relay.

## Rotate keys without downtime

A per-org **signing key** signs every relay token. To rotate keys
without downtime:

1. Call `streams.signingKeys.create({ orgId, label })`. This issues a new
   active key.
2. Wait until the clients' old tokens expire (1h by default).
3. Call `streams.signingKeys.retire({ id })`. The old key then mints no
   new tokens.

The relay hydrates the active set of public keys from Redis on a 30 s
refresh. Retirements take effect within that window.

## Legacy: service-account JWT

The original control plane (the `ClutchCallClient` root import:
`dial`, `hangup`, `barge`, `push_audio`) authenticated with an RSA
service-account JSON file. `CLUTCHCALL_CREDENTIALS` points at that file:

```json
{
  "tenant_id": "acme",
  "private_key": "-----BEGIN PRIVATE KEY-----\n…\n-----END PRIVATE KEY-----",
  "private_key_id": "kid-2026-01"
}
```

The SDK signed an RS256 JWT (`iss=clutchcall-sdk`, `sub=tenant_id`)
and presented it on the QUIC handshake. This still works for backwards
compat. New code should use API keys + relay tokens via the Voice
modality.

## Related

- [Modalities overview](/modalities/overview)
- [Architecture](/concepts/architecture) — where namespace_auth fits in the relay
- [Streams reference](/sdks/typescript/reference) — `apiKeys`, `signingKeys`
