+ The zebra pages (the chat and the voice
+ call) need exactly two things from a server: a rendezvous relay
+ so two browsers can find each other, and a TURN server so they
+ can still connect when both sit behind NAT. Everything else — the crypto,
+ the modem, the audio — runs in the browser. This page hands you the whole
+ back end so you can run it for your own community on a single small box.
+
+
+
+ This is the exact infrastructure behind www.unturf.com/zebra-report,
+ written out so you can stand up your own. It is a gift — reproduce it,
+ fork it, harden it. Your members then point the existing pages at your servers
+ with two URL parameters; nothing about the client needs to change.
+
+ One internet-facing box (a $5–$6/mo VPS is plenty for a small community)
+ running three daemons behind a TLS reverse proxy:
+
+
+
coturn — the STUN/TURN server. STUN tells a browser
+ its public address; TURN relays the encrypted media when a direct path is
+ impossible. This is the part that makes calls work across mobile networks and
+ strict NATs.
+
a rendezvous relay — a tiny WebSocket service that
+ pairs two browsers in a "room" and forwards their encrypted connection setup.
+ It never sees plaintext: the room id is an opaque hash and the setup data is
+ encrypted with the shared code before it ever reaches the server.
+
a credential mint — a single HTTP endpoint that hands
+ each browser a short-lived TURN username/password, so every user draws on
+ their own quota instead of sharing one static login.
+
+
+ The mint and the relay are the same small program here, but they are independent
+ — split them if you like. coturn is off-the-shelf.
+
+ When the network allows it, the two browsers talk directly and
+ coturn never touches the media. coturn is the fallback that guarantees a
+ connection; the rendezvous relay is only used for the few hundred bytes of
+ setup, then sits idle.
+
+
+
3 · coturn — the TURN relay
+
+ Install it (apt install coturn on Debian/Ubuntu) and replace
+ /etc/turnserver.conf with this. Swap in your box's public IP and a
+ DNS name you control:
+
+
# /etc/turnserver.conf
+external-ip=YOUR.PUBLIC.IP
+relay-ip=YOUR.PUBLIC.IP
+listening-port=3478
+realm=turn.example.com
+
+# relay allocation range — open these UDP ports in your firewall too
+min-port=49152
+max-port=50151
+
+# time-limited credentials: the mint computes HMAC-SHA1(secret, expiry).
+# the secret is appended below at deploy and never committed.
+use-auth-secret
+# static-auth-secret=<injected at deploy, see step 4>
+
+# abuse quotas — per ephemeral user, so they stay tight as you scale
+total-quota=2000
+user-quota=6
+bps-capacity=400000000
+max-bps=2000000
+stale-nonce=600
+
+fingerprint
+no-cli
+no-loopback-peers
+no-multicast-peers
+log-file=/var/log/coturn/coturn.log
+simple-log
+
+ Run it under systemd as an unprivileged user (all ports are above 1024, so no
+ special capabilities are needed):
+
+ Firewall: allow inbound UDP 3478 (and TCP 3478 if you
+ offer TCP relay) plus the whole UDP 49152-50151 range. On a cloud
+ provider, that means a firewall rule, not just ufw.
+
+
+
4 · the shared secret
+
+ coturn and the mint share one secret. The mint signs each ephemeral credential
+ with it; coturn validates against the same value. Generate your own
+ — never reuse anyone else's, never print it, never commit it:
+
+
# run once, as root, on the box
+umask 077
+openssl rand -hex 32 > /etc/zebra-turn-secret
+chmod 600 /etc/zebra-turn-secret
+
+SECRET=$(cat /etc/zebra-turn-secret)
+
+# wire it into coturn
+sed -i '/^static-auth-secret=/d' /etc/turnserver.conf
+printf 'static-auth-secret=%s\n' "$SECRET" >> /etc/turnserver.conf
+
+# and into the mint's environment
+printf 'ZEBRA_TURN_SECRET=%s\n' "$SECRET" > /etc/zebra-signal.env
+chmod 640 /etc/zebra-signal.env
+
+ Generate once and persist it: rotating the secret invalidates every credential
+ already handed out, dropping live calls. Keep the file 600, owned
+ by root.
+
+
+
5 · minting credentials — /turn-cred
+
+ This is the only non-obvious piece, and it is tiny. coturn's
+ use-auth-secret mode accepts any username whose value is a future
+ unix timestamp, with the password being
+ base64(HMAC‑SHA1(secret, username)). So the endpoint just
+ stamps an expiry and signs it. In Go:
+
+ That Access-Control-Allow-Origin: * matters: it is what lets a
+ browser on any page — including a copy of the zebra page saved to disk and
+ opened from file:// — fetch a credential. The credential is
+ short-lived and per-user, so handing it out openly is by design.
+
+
+
6 · the rendezvous relay
+
+ The relay is a stateless WebSocket server, a few hundred lines of standard
+ library, no database. Its whole job:
+
+
+
A browser connects to /zebra-signal?room=<hash>. The room
+ is a SHA-256 of the shared code, so the server learns nothing about the code.
+
The first peer in a room is told it is the offerer; the second is the
+ answerer. (Or "whoever is already present offers when the other joins" —
+ either rule works, as long as it is deterministic.)
+
Every message a peer sends is forwarded verbatim to the other peer in the
+ same room. The payload is the WebRTC offer/answer, already encrypted
+ in the browser with a key derived from the shared code (PBKDF2 → AES-GCM).
+ The relay forwards ciphertext it cannot read.
+
The server sends periodic WebSocket pings so idle calls don't get reaped by
+ intermediaries, and drops a room when both peers leave.
+
+
+ Run it under systemd as an unprivileged user, reading the secret from the env
+ file written in step 4:
+
+ Browsers require wss:// (TLS) for WebSockets and a secure context
+ for the crypto, so put a reverse proxy in front that terminates TLS. With
+ Caddy you get automatic certificates and
+ the config is four lines:
+
+ Caddy fetches a Let's Encrypt certificate on first request. The WebSocket
+ upgrade is proxied transparently; the * CORS header set by the mint
+ passes straight through. That is the entire edge.
+
+
+
8 · point your members at it
+
+ Now the payoff: nobody needs a modified page. The published
+ zebra pages read two URL parameters and fall back to the unturf servers only if
+ they are absent. Send your community a link with your own endpoints:
+
+ Or host the page yourself (it is a single self-contained HTML file) and serve it
+ from the same box. Either way, the call is established through your
+ relay and, when needed, relayed through your coturn. The same two
+ parameters work on the text chat (index.html) and the voice call
+ (zebra-audio.html).
+
+
+ Saved a copy to disk? It still works from file:// — the crypto
+ runs in a secure context and the * CORS header lets the saved file
+ fetch credentials — as long as your relay and TURN server are reachable.
+
+
+
9 · how many users can it carry
+
+ The rendezvous relay is nearly free: it moves a few hundred bytes per call setup
+ and then idles, so a tiny box pairs thousands of rooms. coturn is the
+ ceiling, and only for relayed calls (direct peer-to-peer calls cost it
+ nothing). Each relayed voice call is bidirectional audio — tens of kbit/s
+ per leg. With bps-capacity=400000000 (400 Mbit/s) the limit is
+ whatever your VPS's actual uplink and monthly transfer allow, long before coturn
+ itself strains.
+
+
+ The user-quota and total-quota lines cap concurrent
+ allocations to blunt abuse. Raise total-quota as you grow; keep
+ user-quota small (a handful of allocations per credential is plenty
+ for one call). Because credentials are per-user and expire, a leaked one is
+ worthless within hours.
+
+
+
+
+
10 · it's a gift
+
+ This stack is open intellectual capital — take it and run a community the
+ unturf servers will never see or meter. Patch it, harden it, pass it on. Every
+ box that runs its own relay makes the whole mesh more resilient and less
+ centralised, which is the entire point.
+