Skip to main content

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:

CapabilityAPI used
Presence — see who joins / leaves the room in real timepeer.on("peer-joined" \| "peer-left", …)
Chat — broadcast messages to everyone in the roompeer.send({ type: "chat", text }) + peer.on("data", …)
WebRTC video — peer-to-peer camera + mic, automatic meshpeer.addStream(localStream) + remote.on("stream-added", …)
Auto-injected TURN — works behind symmetric NAT with zero configBuilt in; just have a TURN service on your account
Connection status — live connecting → connected → reconnecting indicatorpeer.on("state-change", …)

How to run it

  1. Get a publishable key from dashboard.metered.ca → Realtime Messaging → Keys → Create key (type: Publishable). Tick the Send checkbox under "What can this key do?" before savingSend is off by default for publishable keys, but the WebRTC layer of @metered-ca/realtime uses it under the hood for SDP / ICE exchange between peers. Without it, the demo connects to the channel but never negotiates the video stream.
  2. Open the live demo in your browser, view source, and copy it into a local HTML file. Replace pk_live_REPLACE_ME with your key.
  3. Serve the file over HTTPS or http://localhost:* (browsers block getUserMedia on plain http:// from non-localhost). Any static server works:
npx serve .
# or
python3 -m http.server 3000
  1. 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-change listener 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 tokenProvider plumbing
  • 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