Skip to main content

WebRTC Video Call — No Backend

Same WebRTC call as the main guide, but without a server to mint JWTs. Suitable for:

  • Static sites, single-page demos, prototypes
  • Internal tools where every user shares the same scope
  • Examples + tutorials

What you trade away by going backend-less:

  • No per-user peerId — server assigns a UUID each connect. If your app needs stable per-user IDs, mint JWTs.
  • No peerMetadata — presence events don't carry usernames or avatars. You'd have to broadcast them yourself after connecting (peer.send({ type: "introduce", username, avatar })).
  • All peers share the key's permissions — no per-user scoping.

If those trade-offs are OK, this path saves you from running any server-side code.

Get a pk_live_ key

  1. Dashboard → Realtime Messaging → Keys → Create key, type publishable.
  2. Set:
    • Channels: wildcards your demo will use, e.g. demo-room-*
    • Actions: subscribe, publish, presence, send
    • Allowed origins: the URL of your static site, e.g. https://demo.example.com
  3. Copy the pk_live_… value.

Get TURN credentials, client-side

For a real WebRTC connection between users on cellular / corporate networks, you'll need TURN. Without a backend, fetch TURN credentials directly from the Metered TURN REST API.

The TURN REST API's GET /api/v1/turn/credentials endpoint returns a credential set. Call it from your browser code:

async function fetchIceServers() {
const r = await fetch(
`https://YOUR_TURN_APP.metered.live/api/v1/turn/credentials?apiKey=YOUR_TURN_API_KEY`,
);
return r.json(); // [{ urls: "stun:..." }, { urls: "turn:...", username, credential }, ...]
}

This call is rate-limited and the resulting creds are short-lived (a few hours). For a long-running app, refresh them before they expire — or use the JWT path so they auto-rotate via tokenProvider.

Browser code

call.html
<!DOCTYPE html>
<html>
<body>
<video id="local" autoplay playsinline muted></video>
<div id="remotes"></div>
<script type="module" src="./call.js"></script>
</body>
</html>
call.js
import { MeteredPeer } from "@metered-ca/peer";

const PK_KEY = "pk_live_…";
const TURN_APP_KEY = "your-turn-app-key";

// pk_live_ keys don't include iceServers, so we inject them via the
// rtcPeerConnectionFactory hook on construction. The factory receives
// the SDK's RTCConfiguration and returns the wired-up RTCPeerConnection.
const iceServers = await fetchIceServers();

const peer = new MeteredPeer({
apiKey: PK_KEY,
rtcPeerConnectionFactory: (cfg) => new RTCPeerConnection({
...cfg,
iceServers, // your client-side-fetched TURN creds
}),
});

const localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
document.querySelector("#local").srcObject = localStream;
peer.addStream(localStream);

const tiles = new Map();

peer.on("peer-joined", ({ peer: remote }) => {
const tile = document.createElement("video");
tile.autoplay = true;
tile.playsInline = true;
document.querySelector("#remotes").appendChild(tile);
tiles.set(remote.id, tile);

remote.on("track", ({ streams }) => {
tile.srcObject = streams[0];
});
});

peer.on("peer-left", ({ peer: remote }) => {
tiles.get(remote.id)?.remove();
tiles.delete(remote.id);
});

// Identify yourself via a broadcast (since there's no peerMetadata).
peer.on("joined", () => {
peer.send({ type: "introduce", username: "Alice" });
});

const usernames = new Map(); // peerId → username
peer.on("data", ({ senderPeerId, data }) => {
if (data.type === "introduce") {
usernames.set(senderPeerId, data.username);
refreshTileLabel(senderPeerId);
}
});

async function fetchIceServers() {
const r = await fetch(
`https://YOUR_TURN_APP.metered.live/api/v1/turn/credentials?apiKey=${TURN_APP_KEY}`,
);
return r.json();
}

await peer.join("demo-room-42");

That's the complete app. Drop it on Netlify, share the URL, two visitors get a working call.

What about pure presence (no TURN)?

If you don't need WebRTC — just chat, telemetry, pub/sub — skip the TURN fetch entirely. MeteredPeer works fine without iceServers, and peer.send(data) doesn't require any peer-to-peer connectivity (it's server-routed). The rtcPeerConnectionFactory hook is only needed when peers will actually exchange media.

Pitfalls

  1. No peerMetadata — peers don't know each other's names. Use a broadcast introduce message on joined (as above). Every peer learns every other peer's name once they introduce. New peers joining later won't know existing peers until you re-broadcast — listen for peer-joined and re-send.

  2. TURN credentials expire mid-call. Client-side-fetched TURN creds last a few hours. If a call runs longer, ICE will eventually fail. To recover, refresh the creds and rebuild — await peer.close(), fetch fresh iceServers, construct a new MeteredPeer. (Or use the JWT path, where the SDK does this for you on reconnect.)

  3. allowedOrigins blocking the WS. Server compares the WS's Origin header to the key's allowedOrigins. If you forget to add your deploy URL, you get 4001 on every connect. Check the dashboard.

  4. CORS on the TURN REST API. Browsers send a preflight. The TURN REST endpoint allows the necessary origins by default, but if you've front-ended it with a reverse proxy, double-check the proxy's CORS headers.

  5. Same key used in source + deployed app + open-source repos. A pk_live_ is meant to be public, but its allowedOrigins is the only access control. If you hardcode it in a repo and forget to scope allowedOrigins, anyone running the SDK against your key from localhost could connect. Scope tightly.

  6. Trusting data.from (or anything in data) for identity. Since there's no peerMetadata, you might be tempted to put a from: "alice" field inside data and trust it. Don't — anyone in the channel can lie about that. Use the envelope-level senderPeerId as the canonical sender; the SDK won't surface data.from separately.

When to graduate to the JWT path

If your app reaches any of:

  • Multiple users with different permissions (admin vs viewer)
  • Need for stable per-user peerId across sessions
  • Per-user TURN credentials (separate billing buckets, rotating creds)
  • Production with paying users

…switch to tokenProvider. The browser code barely changes; you just add a backend route that mints the JWT. See the main WebRTC guide and Authentication.

See also