Single-File HTML Demo
The smallest possible working demo. One HTML file, no npm, no bundler, no backend, no JWT minting. Save the file, replace the API key, open in two browser tabs — peer-to-peer video + chat + presence works.
Open the live demo → · Download the HTML file → (right-click → Save As)
What it demonstrates
Five capabilities, all in a single 200-line HTML file with vanilla JS:
| Capability | API used |
|---|---|
| Presence — see who joins / leaves the room in real time | peer.on("peer-joined" \| "peer-left", …) |
| Chat — broadcast messages to everyone in the room | peer.send({ type: "chat", text }) + peer.on("data", …) |
| WebRTC video — peer-to-peer camera + mic, automatic mesh | peer.addStream(localStream) + remote.on("stream-added", …) |
| Auto-injected TURN — works behind symmetric NAT with zero config | Built in; just have a TURN service on your account |
Connection status — live connecting → connected → reconnecting indicator | peer.on("state-change", …) |
How to run it
- Get a publishable key from dashboard.metered.ca → Realtime Messaging → Keys → Create key (type: Publishable). Tick the
Sendcheckbox under "What can this key do?" before saving —Sendis off by default for publishable keys, but the WebRTC layer of@metered-ca/realtimeuses it under the hood for SDP / ICE exchange between peers. Without it, the demo connects to the channel but never negotiates the video stream. - Open the live demo in your browser, view source, and copy it into a local HTML file. Replace
pk_live_REPLACE_MEwith your key. - Serve the file over HTTPS or
http://localhost:*(browsers blockgetUserMediaon plainhttp://from non-localhost). Any static server works:
npx serve .
# or
python3 -m http.server 3000
- Open
http://localhost:3000/(or wherever you served the file) in two browser tabs. Allow camera + mic in both. They'll connect peer-to-peer.
The chat side of the demo works even without camera permission — useful for testing from a sandboxed environment or a machine without a webcam.
What the demo doesn't include (and why)
To keep the file under 200 lines and readable on first inspection, the demo deliberately leaves out:
- Per-user identity — every peer is identified by a random server-assigned UUID. For stable
peerIds tied to your user accounts, switch to the JWT path. See Authentication. - Mute / camera-off controls — see WebRTC Video Call guide for the full UX pattern (Mute button, camera-off, screen-share).
- Reconnect UI — the SDK reconnects automatically; surfacing a "reconnecting…" banner is one
state-changelistener away. See Reconnect Best Practices. - Production error handling — the demo logs errors to the chat panel; a real app would route them to a toast / Sentry / etc.
For a fuller end-to-end app with all of these, jump to the WebRTC Video Call guide (which uses the JWT path) or WebRTC — No Backend (pk_-only, but layered).
Why pk_live_ for a demo
The pk_live_ (publishable) key path is intentionally low-friction:
- No backend needed — the key goes directly in the browser
- No JWT minting, no token expiry, no
tokenProviderplumbing - Permissions and channel scopes are baked into the key at dashboard time
- TURN credentials are auto-injected by the signalling server (when "Auto-inject TURN" is on, the default) — your WebRTC works behind symmetric NAT without any client-side TURN fetch
The downside: every peer connecting with this key gets a fresh random peerId (no stable identity tied to your user accounts) and the same channel permissions. For production apps with user accounts, layer on JWTs. See Authentication.
See also
- Getting Started — the structured introduction
- Basic Video Call example — same idea, walked through section by section
- WebRTC — No Backend guide — the production-grade pk_-only path with reconnect, error handling, etc.
- Authentication — JWT vs pk_, when to graduate