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
- Dashboard → Realtime Messaging → Keys → Create key, type
publishable. - 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
- Channels: wildcards your demo will use, e.g.
- 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
<!DOCTYPE html>
<html>
<body>
<video id="local" autoplay playsinline muted></video>
<div id="remotes"></div>
<script type="module" src="./call.js"></script>
</body>
</html>
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
No
peerMetadata— peers don't know each other's names. Use a broadcastintroducemessage onjoined(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 forpeer-joinedand re-send.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 freshiceServers, construct a newMeteredPeer. (Or use the JWT path, where the SDK does this for you on reconnect.)allowedOriginsblocking the WS. Server compares the WS'sOriginheader to the key'sallowedOrigins. If you forget to add your deploy URL, you get 4001 on every connect. Check the dashboard.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.
Same key used in source + deployed app + open-source repos. A
pk_live_is meant to be public, but itsallowedOriginsis the only access control. If you hardcode it in a repo and forget to scopeallowedOrigins, anyone running the SDK against your key fromlocalhostcould connect. Scope tightly.Trusting
data.from(or anything indata) for identity. Since there's nopeerMetadata, you might be tempted to put afrom: "alice"field insidedataand trust it. Don't — anyone in the channel can lie about that. Use the envelope-levelsenderPeerIdas the canonical sender; the SDK won't surfacedata.fromseparately.
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
peerIdacross 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.