Quickstart
Connect a prototype with one API key, then add production trust when needed
Install
pnpm add openrtcCreate a lazy client
import { OpenRTC } from 'openrtc';
const rtc = OpenRTC({
apiKey: import.meta.env.VITE_OPENRTC_API_KEY,
});This line has no side effects. It is safe to construct during application setup.
Activate one avenue
For a backend-free prototype:
const space = await rtc.spaces.join('demo-cursors');
const cursors = space.state<{ x: number; y: number }>('cursor');
cursors.watch(({ peerId, value }) => renderCursor(peerId, value));
cursors.set({ x, y });For an authenticated device mesh:
const devices = await rtc.devices.start({ auth: myAuthProvider });For a group session:
const room = await rtc.rooms.join('match-123', {
access: 'capability',
identity: 'session',
payload: 'latest-state',
});Each anonymous tab gets a new memory-only signing identity. Tabs can join the same room as different peers without persisting or sharing that session key.
For a room that may grow, keep the same API and let OpenRTC choose the safe,
lowest-credit eligible shape. auto is the default, so spelling it out is
optional:
const audience = await rtc.rooms.join('community-stage', {
architecture: 'auto',
payload: 'reliable',
});
await audience.channel('chat').send({ text: 'Hello everyone' });Small groups stay full mesh. Reliable larger rooms use content-blind managed
fan-out when enabled. Rooms that declare payload: 'latest-state' may switch to
bounded sparse fan-out through 50 members. Unsupported or unfunded shapes fail
closed. Your app never chooses neighbors or transports. See
Adaptive large rooms.
For native edge provisioning without a cloud connection:
import { OpenRTC as OpenRTCNative } from 'openrtc/native';
const nativeRtc = OpenRTCNative({
apiKey: import.meta.env.VITE_OPENRTC_API_KEY,
});
const support = await nativeRtc.offline.support();
if (support.localMesh) {
const request = await nativeRtc.offline.createEnrollmentRequest({
trustDomain: 'field-team-a',
requestedRoles: ['sensor'],
});
displayQrCode(request);
}This creates a public request with the native Rust signer. It does not expose the private key or make LAN discovery count as authorization. See Offline edge devices.
Add audio and video
If you chose a room, add ordinary browser media tracks to each peer connection.
This compact example assumes localVideo and remoteVideo are HTML video
elements:
const localMedia = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
});
localVideo.srcObject = localMedia;
const remoteMedia = new MediaStream();
remoteVideo.srcObject = remoteMedia;
room.onConnection(async (connection) => {
connection.media.onTrack(({ track }) => {
for (const current of remoteMedia.getTracks()) {
if (current.kind === track.kind) remoteMedia.removeTrack(current);
}
remoteMedia.addTrack(track);
});
for (const track of localMedia.getTracks()) {
await connection.media.addTrack(track);
}
});OpenRTC keeps these tracks on the logical peer connection while Rust chooses and recovers the physical carrier. See Media for HTML, cleanup, call controls, group calls, browser support, and native Rust.
For one-to-many live video, open one broadcast instead of one peer connection per viewer. The short-lived grant from your backend decides the role and hard limits:
const live = await rtc.broadcasts.open(broadcastGrant);
if (live.role === 'owner' || live.role === 'publisher') {
for (const track of localMedia.getTracks()) await live.media.addTrack(track);
}
live.media.onTrack(({ track, source }) => {
renderAnonymousSource(source.id, track);
});See Live broadcasting for co-hosts, privacy, and the current developer-preview availability.
For a bounded, manually connected handoff:
const ticket = await rtc.tickets.issue('invite-42', { maxPeers: 2 });Close what you started
await space.leave();
await devices.close();
await room.leave();
await ticket.close();
localMedia.getTracks().forEach((track) => track.stop());
await rtc.close();Fine-tune only when needed
The omitted options above are intentional. Spaces default to capability access
and ephemeral identity; rooms default to capability access and ephemeral
membership; devices default to connecting only to online peers. state()
automatically coalesces replaceable cursor or transform updates.
See Avenues for the complete TypeScript and Rust examples.
Add production identity
Implement AuthProvider with a short-lived assertion from your registered OIDC/JWKS provider. OpenRTC exchanges it for an app-scoped principal and keeps the gateway grant fresh while the handle is active.
import type { AuthProvider } from 'openrtc';
const auth: AuthProvider = {
getAssertion: ({ forceRefresh }) => session.getOpenRtcAssertion({ forceRefresh }),
subscribe: (listener) => session.onIdentityChanged(listener),
};
Consumer backends validating platform attestation and issuing a registered assertion are the canonical production path. Optional managed adapters are available when you want OpenRTC to validate reusable Firebase App Check, Apple App Attest, or Play Integrity evidence during enrollment. DeviceCheck is an explicitly reduced-trust fallback; none of these providers becomes a connection heartbeat.
Opt into advanced managed features
The portal manifest must enable an advanced feature before runtime code may request it. A local session ceiling can additionally fail early:
const rtc = OpenRTC({
apiKey,
usage: {
warnAtSessionCreditsUsd: 0.25,
maxSessionCreditsUsd: 0.50,
onWarning: console.warn,
},
transports: { iroh: true, webrtc: true, relay: false },
});
The server budget remains authoritative.
relay: false is a direct-only policy for this client. It removes Iroh relay,
relayed WebRTC ICE, and MoQ but keeps eligible direct ICE for the webrtc
Iroh carrier and native direct routes. A
browser behind restrictive NAT may not connect. Omit relay to keep the
default managed fallback; client code cannot enable relay when the Developer
Portal has disabled it for the app.