1# commonroom-turn
3A Cloudflare Worker that mints short-lived TURN credentials for CommonRoom.
5Most WebRTC connections pair up directly, but some cannot: symmetric NAT at
6both ends, or a firewall that only permits outbound 443. Those pairs need a
7relay, and CommonRoom uses [Cloudflare Realtime
8TURN](https://developers.cloudflare.com/realtime/turn/) for it. Relaying is
9metered, so the credentials are gated behind a token rather than handed to
10anyone who loads the page.
12The Worker exists because the browser cannot call Cloudflare's TURN API
13itself: doing so would put the long-lived API token in a static bundle, where
14anyone could mint unlimited credentials against the account. The Worker holds
15that token, checks a room token, and returns only a short-lived ICE
16configuration.
18```
19POST / Authorization: Bearer <room token>
20{"room": "<opaque tag, optional>"}
21-> 200 {"iceServers": [...], "expiresAt": <epoch ms>}
22```
24## Setup
261. **Create a TURN key.** In the Cloudflare dashboard, go to Realtime → TURN
27 and create a key. You get a TURN key ID and an API token.
292. **Choose room tokens.** These are the strings people type into CommonRoom's
30 landing form. Any hard-to-guess strings will do:
32 ```sh
33 openssl rand -base64 24
34 ```
36 Several can be configured at once (comma-separated), so different groups can
37 have different tokens and one can be withdrawn without disturbing the others.
393. **Set the secrets and deploy.**
41 ```sh
42 cd worker
43 npm install
44 npx wrangler secret put TURN_KEY_ID
45 npx wrangler secret put TURN_KEY_API_TOKEN
46 npx wrangler secret put ROOM_TOKENS
47 npx wrangler deploy
48 ```
504. **Point the app at it.** Edit `ALLOWED_ORIGINS` in `wrangler.jsonc` to the
51 origin serving CommonRoom, then set `VITE_TURN_ENDPOINT` to the deployed
52 Worker URL — in `.env.local` for local development, and as the repository
53 variable `VITE_TURN_ENDPOINT` for the GitHub Pages build. That URL is not a
54 secret; the room token is what protects the endpoint.
56## Local development
58```sh
59cp .dev.vars.example .dev.vars # then edit in your real key ID and API token
60npm run dev
61```
63`.dev.vars` is gitignored. With the example values left as they are, token
64checks work (`goodtoken` is accepted, anything else gets a 401) but the
65upstream call to Cloudflare returns 404, which the Worker reports as a 502 —
66enough to exercise everything except the credential itself.
68## Cost
70Cloudflare Realtime TURN is $0.05/GB after a free tier of 1,000 GB per month,
71billed on data sent from the Cloudflare edge to the TURN client. A relayed
72participant in a four-person room at the default `medium` quality preset
73receives roughly 1 GB per hour, so the free tier covers on the order of 800
74participant-hours per month. Only the connections that actually need a relay
75use one; everyone else pairs directly and costs nothing.
77Usage is visible per room in the Realtime analytics: the client sends a prefix
78of the hashed room topic as the credential's `customIdentifier`, which
79distinguishes rooms from each other without revealing any room name.
81## Notes on the token
83A room token buys a credential, so treat it as a spending key. It is checked in
84constant time, and `CREDENTIAL_TTL` bounds how long an issued credential stays
85useful (Cloudflare's maximum is 48 hours). Credentials can also be revoked
86before they expire:
88```sh
89curl --request POST \
90 https://rtc.live.cloudflare.com/v1/turn/keys/$TURN_KEY_ID/credentials/$USERNAME/revoke \
91 --header "Authorization: Bearer $TURN_KEY_API_TOKEN"
92```
94Note that a credential minted by one participant is shared with everyone else
95in that room (this is the point — only one person needs a token), so anyone in
96the room could in principle use it elsewhere until it expires. Keeping
97`CREDENTIAL_TTL` modest is the main defense; per-room analytics is how abuse
98would be noticed.