Skip to main content

Authentication

Two key types, each suited to a different security boundary:

KeyPrefixWhere it livesWhat it authorizes
Publishablepk_live_…Browser / mobile / shipped binaryDirect WS connect with fixed channel patterns + actions (configured on the dashboard)
Secretsk_live_…Your backend ONLYMints 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 of publish / subscribe / presence / send
  • allowedOrigins — browser Origin header 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:

  1. Self-mint — your backend signs with the sk_'s signing secret directly. See the Quickstart.
  2. Call the REST APIPOST /v1/tokens with 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"
}
}
ClaimRequiredWhat it does
subThe peer's stable identity. Becomes peerId server-side. Printable ASCII, ≤128 chars.
expUnix seconds. Server force-closes the connection ~250ms before this. Capped at 24h.
iatStandard JWT issued-at. Used for analytics.
issIssuer string. Free-form.
channelsChannels 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.
permissionsSubset of publish / subscribe / presence / send. Defaults to the key's full action set if omitted.
metadataConnection-private. Delivered ONCE in the welcome message; never reaches other peers. WebRTC customers tuck iceServers here. See Presence & Metadata.
peerMetadataPer-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:

HTTPReason
401Invalid / expired token, key not found, origin not on the allowlist
404Wrong path — only /v1 accepts upgrades
429Per-IP connect rate limit exceeded
503Internal 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