Authentication
Two key types, each suited to a different security boundary:
| Key | Prefix | Where it lives | What it authorizes |
|---|---|---|---|
| Publishable | pk_live_… | Browser / mobile / shipped binary | Direct WS connect with fixed channel patterns + actions (configured on the dashboard) |
| Secret | sk_live_… | Your backend ONLY | Mints JWTs that carry per-peer claims (channels, permissions, identity metadata, TURN credentials) AND drives the REST control plane |
You'll typically use both: the pk for casual browser connect (low-permission), and the sk on your backend to issue JWTs for authenticated users.
The two-key model in pictures
PUBLISHABLE — pk_live_…
┌──────────────┐ ┌───────────┐
│ browser │ ── WS connect ?key=pk_… ───→ │ Metered │
│ (any) │ │ Realtime │
└──────────────┘ ← welcome ────────────────────└───────────┘
fixed channels + actions from the key's dashboard config
SECRET — sk_live_…
┌──────────────┐ POST /v1/tokens ┌───────────┐
│ your backend │ ─── (sk_ Bearer) ──────→ │ Metered │
│ │ │ Realtime │
│ │ ←── { token, expiresAt } ─└───────────┘
│ │ ▲
│ │ ── token via your auth ─→ ┌──────┴─────┐
└──────────────┘ │ client │
└──────┬─────┘
│ WS connect ?token=<jwt>
▼
┌───────────┐
│ Metered │
│ Realtime │
└───────────┘
Publishable keys (pklive…)
For low-trust environments. The keyId IS the credential — there's no separate signing secret.
Connect URL:
wss://rms.metered.ca/v1?key=pk_live_abcdef0123456789…
What the key carries (configured when you create it in the dashboard):
channelPatterns— what channels this key may interact with (e.g.["app_xyz/*"])actions— subset ofpublish/subscribe/presence/sendallowedOrigins— browserOriginheader allowlist (e.g.["https://app.customer.com"])- The server assigns a random peerId at connect time (you can't pick it; the key is too low-trust for that)
Use for: simple read-only or anonymous-publish flows where every user gets the same scope. Public livestream chat where viewers are pseudonymous, anonymous status pages, demos.
Don't use for: anything that depends on a stable user identity. There's no way to pin a pk_ connect to a specific user — for that you need a JWT.
Secret keys (sklive…) and JWTs
For authenticated users. Your backend signs an HS256 JWT carrying the user's identity + permissions; the user opens the WebSocket with ?token=<jwt>.
Two ways to mint a JWT:
- Self-mint — your backend signs with the sk_'s signing secret directly. See the Quickstart.
- Call the REST API —
POST /v1/tokenswith the sk_ as Bearer; Metered mints and returns the JWT. Useful if you'd rather not embed JWT libraries. See REST API → Tokens.
JWT claims
{
"iss": "your-backend",
"sub": "alice@example.com",
"exp": 1715539200,
"iat": 1715535600,
"channels": ["app_abc/room-1", "app_abc/dm-alice-bob"],
"permissions": ["publish", "subscribe", "presence", "send"],
"metadata": {
"iceServers": [
{ "urls": ["stun:stun.relay.metered.ca:80"] },
{ "urls": ["turn:global.relay.metered.ca:80"], "username": "u", "credential": "c" }
]
},
"peerMetadata": {
"userId": "u_alice_123",
"username": "Alice Anderson",
"profilePic": "https://cdn.example.com/u/alice.jpg"
}
}
| Claim | Required | What it does |
|---|---|---|
sub | ✅ | The peer's stable identity. Becomes peerId server-side. Printable ASCII, ≤128 chars. |
exp | ✅ | Unix seconds. Server force-closes the connection ~250ms before this. Capped at 24h. |
iat | Standard JWT issued-at. Used for analytics. | |
iss | Issuer string. Free-form. | |
channels | Channels this peer is pre-authorized to subscribe to. MUST be a subset of the key's channelPatterns — the server rejects mints that try to elevate. | |
permissions | Subset of publish / subscribe / presence / send. Defaults to the key's full action set if omitted. | |
metadata | Connection-private. Delivered ONCE in the welcome message; never reaches other peers. WebRTC customers tuck iceServers here. See Presence & Metadata. | |
peerMetadata | Per-peer identity bag. Stamped onto presence events, direct messages, and (opt-in per-subscribe) channel messages. Use for userId, username, profilePic. |
JWT header
The header MUST include the kid of the sk_ key used to sign:
{ "alg": "HS256", "kid": "sk_id_abc123…", "typ": "JWT" }
The server looks up the signing material by kid when verifying. The kid IS the sk_ keyId — NOT the signing secret (that one stays on your backend forever).
Algorithm — HS256 only
The server accepts HS256 with the key's signing secret. RS256/EdDSA are not currently supported; if you need asymmetric, contact sales.
Key rotation
When you rotate a sk_'s signing secret (via the dashboard or POST /api/internal/v1/signalling/keys/:keyId/rotate):
- The keyId stays the same. JWTs already in flight (with the same
kid) keep verifying. - The old signing secret remains valid for 24 hours (configurable grace window). After that, old JWTs fail with
token_expired. - Update your backend to sign with the NEW secret as soon as you rotate.
Pre-handshake rejections
Auth failures are rejected at the HTTP layer (no WebSocket handshake completes), so clients see them as HTTP 401 rather than a WS close frame:
| HTTP | Reason |
|---|---|
| 401 | Invalid / expired token, key not found, origin not on the allowlist |
| 404 | Wrong path — only /v1 accepts upgrades |
| 429 | Per-IP connect rate limit exceeded |
| 503 | Internal infra failure during auth |
After the WS handshake completes, runtime authorization failures (channel_not_authorized, action_not_permitted) come back as error messages with the connection staying open.
What's next
- Channels — patterns + wildcards
- Presence & Metadata — the
peerMetadatastory - WebRTC Signalling guide — the marquee end-to-end TURN + signalling pattern