Quickstart

Connect a prototype with one API key, then add production trust when needed

Install

pnpm add openrtc

Create 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.