| .. | ||
| src | ||
| .dev.vars.example | ||
| .gitignore | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| wrangler.jsonc |
commonroom-turn#
A Cloudflare Worker that mints short-lived TURN credentials for CommonRoom.
Most WebRTC connections pair up directly, but some cannot: symmetric NAT at both ends, or a firewall that only permits outbound 443. Those pairs need a relay, and CommonRoom uses Cloudflare Realtime TURN for it. Relaying is metered, so the credentials are gated behind a token rather than handed to anyone who loads the page.
The Worker exists because the browser cannot call Cloudflare's TURN API itself: doing so would put the long-lived API token in a static bundle, where anyone could mint unlimited credentials against the account. The Worker holds that token, checks a room token, and returns only a short-lived ICE configuration.
POST / Authorization: Bearer <room token>
{"room": "<opaque tag, optional>"}
-> 200 {"iceServers": [...], "expiresAt": <epoch ms>}
Setup#
-
Create a TURN key. In the Cloudflare dashboard, go to Realtime → TURN and create a key. You get a TURN key ID and an API token.
-
Choose room tokens. These are the strings people type into CommonRoom's landing form. Any hard-to-guess strings will do:
openssl rand -base64 24Several can be configured at once (comma-separated), so different groups can have different tokens and one can be withdrawn without disturbing the others.
-
Set the secrets and deploy.
cd worker npm install npx wrangler secret put TURN_KEY_ID npx wrangler secret put TURN_KEY_API_TOKEN npx wrangler secret put ROOM_TOKENS npx wrangler deploy -
Point the app at it. Edit
ALLOWED_ORIGINSinwrangler.jsoncto the origin serving CommonRoom, then setVITE_TURN_ENDPOINTto the deployed Worker URL — in.env.localfor local development, and as the repository variableVITE_TURN_ENDPOINTfor the GitHub Pages build. That URL is not a secret; the room token is what protects the endpoint.
Local development#
cp .dev.vars.example .dev.vars # then edit in your real key ID and API token
npm run dev
.dev.vars is gitignored. With the example values left as they are, token
checks work (goodtoken is accepted, anything else gets a 401) but the
upstream call to Cloudflare returns 404, which the Worker reports as a 502 —
enough to exercise everything except the credential itself.
Cost#
Cloudflare Realtime TURN is $0.05/GB after a free tier of 1,000 GB per month,
billed on data sent from the Cloudflare edge to the TURN client. A relayed
participant in a four-person room at the default medium quality preset
receives roughly 1 GB per hour, so the free tier covers on the order of 800
participant-hours per month. Only the connections that actually need a relay
use one; everyone else pairs directly and costs nothing.
Usage is visible per room in the Realtime analytics: the client sends a prefix
of the hashed room topic as the credential's customIdentifier, which
distinguishes rooms from each other without revealing any room name.
Notes on the token#
A room token buys a credential, so treat it as a spending key. It is checked in
constant time, and CREDENTIAL_TTL bounds how long an issued credential stays
useful (Cloudflare's maximum is 48 hours). Credentials can also be revoked
before they expire:
curl --request POST \
https://rtc.live.cloudflare.com/v1/turn/keys/$TURN_KEY_ID/credentials/$USERNAME/revoke \
--header "Authorization: Bearer $TURN_KEY_API_TOKEN"
Note that a credential minted by one participant is shared with everyone else
in that room (this is the point — only one person needs a token), so anyone in
the room could in principle use it elsewhere until it expires. Keeping
CREDENTIAL_TTL modest is the main defense; per-room analytics is how abuse
would be noticed.