- Control plane (control-plane API tRPC: voice control, streams CRUD, agents, webhooks, analytics) — 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) — short-lived relay tokens scoped to a set of namespaces, verified by the relay’s namespace_auth hook on every publish or subscribe.
Control-plane: API keys
API keys are created in the dashboard or viastreams.apiKeys.create({ orgId, label, scopes }). The key string is
returned once, at creation; the control-plane API stores only the hash.
Data-plane: relay tokens
The relay never sees the API key. The control-plane API mints a short-lived relay token — a signed JWT carrying namespace claims — and the client presents that during the MoQT handshake. The relay’snamespace_auth hook verifies
the JWT signature, extracts the namespace scope, and gates every subsequent
publish / subscribe against it.
A typical relay token looks like:
| 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). |
Browser tokens
In the browser, you cannot ship a long-lived API key. Instead, your server-side mints a short-lived relay token for each browser session and the SPA passes it in:browserToken skips the API-key-on-client antipattern; the SDK presents
it directly to the relay.
Signing-key rotation
Every relay token is signed with a per-org signing key. Rotate keys without downtime by:streams.signingKeys.create({ orgId, label })— issues a new active key- wait for clients with old tokens to expire (1h by default)
streams.signingKeys.retire({ id })— old key stops minting new tokens
Legacy: service-account JWT
The original control plane (theClutchCallClient root import — dial,
hangup, barge, push_audio) authenticated with an RSA service-account
JSON file pointed at by CLUTCHCALL_CREDENTIALS:
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
- Architecture — namespace_auth fits into the relay
- Streams reference —
apiKeys,signingKeys

