API overview

The TypeScript, Rust, and Tauri APIs at a glance

Start with the public client

import { OpenRTC } from 'openrtc';

const rtc = OpenRTC({
  apiKey: import.meta.env.VITE_OPENRTC_API_KEY,
});

Creating the client does not start a connection or load the browser runtime. Network work begins when you start or join an avenue.

The complete option shape is OpenRTC({ apiKey, auth, trust, usage, transports }). Only apiKey is required; add the other options when the application needs them.

NamespaceMethodResult
devicesstart(options)Persistent device-mesh handle
spacesjoin(id, options)Live-only space handle
roomsjoin(id, options)Ephemeral or durable room handle
ticketsissue(id, options)Bounded ticket-session handle
channelsregister(descriptor)Registers a channel for subsequently active handles
usageestimate(input)Local usage-credit estimate
offlinesupport() / createEnrollmentRequest(options)Native edge capability and target-signed enrollment request

Choose the avenue that matches who should meet. Avoid starting a device mesh, space, and room when the feature needs only one of them.

Avenue handles

Every avenue exposes the same core tools:

const room = await rtc.rooms.join('call-123');

room.peers.watch((peers) => renderParticipants(peers));

const chat = room.channel<{ text: string }>('chat');
chat.onMessage(({ peerId, message }) => {
  addMessage(peerId, message.text);
});

await chat.send({ text: 'hello' });

room.onConnection((connection) => {
  console.log('connected to', connection.peerId);
});

await room.leave();

All avenue handles provide close(). Spaces and rooms also provide leave() as a more natural alias. Closing the root client closes every remaining handle. The current ticket API does not expose accept() or server-side revoke().

Connections

A Connection is a stable, logical relationship with one peer. It is not a WebRTC peer connection, MoQ session, or QUIC socket. OpenRTC may replace the physical route while keeping the public connection alive.

room.onConnection((connection) => {
  connection.onMessage((message) => handleMessage(message));
  connection.onClose(() => markPeerOffline(connection.peerId));

  void connection.send({ type: 'ready' });
});

Each connection provides:

APIUse it for
send() / onMessage()Small structured application messages
streamsLong-lived or large byte flows
streams.datagramsFresh values that may be lost, such as live input
mediaAudio and video tracks

OpenRTC keeps avenue scope, admission, application encryption, route recovery, and carrier switching below this API.

Streams

Streams use familiar Web Streams and WebTransport terms. A protocol name lets the receiving application route the stream safely.

const stream = await connection.streams.createBidirectionalStream({
  protocol: 'com.example.chat/1',
});

const writer = stream.writable.getWriter();
await writer.write(new TextEncoder().encode('hello'));
await writer.close();

Media

Browser applications pass ordinary MediaStreamTrack objects. OpenRTC owns the encoded media protocol and keeps it attached to the logical connection.

const local = await navigator.mediaDevices.getUserMedia({
  audio: true,
  video: true,
});
const remote = new MediaStream();
remoteVideo.srcObject = remote;

room.onConnection(async (connection) => {
  for (const track of local.getTracks()) {
    await connection.media.addTrack(track);
  }

  connection.media.onTrack(({ track }) => {
    for (const current of remote.getTracks()) {
      if (current.kind === track.kind) remote.removeTrack(current);
    }
    remote.addTrack(track);
  });
});

Your application does not create SDP, exchange ICE candidates, operate a MoQ session, frame encoded chunks, or restart tracks after an OpenRTC route change.

Native Rust

The openrtc crate works without TypeScript. ControlPlane obtains and renews avenue capabilities. Client owns the native Iroh endpoint, peer lifecycle, messages, streams, datagrams, and media.

use openrtc::{
    Client,
    native::{CapabilityOptions, ControlPlane, DeviceSigner},
};
use std::sync::Arc;

const API_KEY: &str = "pk_live_...";

let signer: Arc<dyn DeviceSigner> = app_secure_signer();
let control = ControlPlane::anonymous(API_KEY, signer)?;

let room = control.join_room(
    "call-123",
    "desktop",
    CapabilityOptions::default(),
).await?;

let client = Arc::new(
    Client::builder(API_KEY.to_owned(), Box::new(|| None))?
        .signaling_backend(room.signaling())
        .build(),
);

client.send_peer(peer_id, br#"{"type":"hello"}"#).await?;

let (_connection_id, _node_id, send, receive) =
    client.open_peer_bi(peer_id, Some(10_000)).await?;

room.close().await;

The host supplies secure key storage, platform capture and codecs, and product authorization. OpenRTC supplies connection and transport infrastructure. See Avenues for all four Rust activation examples.

Tauri without WASM

Tauri WebViews should import the native-only entrypoint. It returns the same TypeScript Client, but sends commands to the native Rust owner over IPC.

import { OpenRTC } from 'openrtc/native';
import { createBridge } from 'openrtc-tauri/ipc';

const rtc = OpenRTC(
  { apiKey, auth },
  createBridge(),
);

The Rust host installs openrtc-tauri-plugin. Product Rust code and the WebView can share that plugin's one Arc<Client>. The frontend does not load WASM, dial peers, retry routes, or select carriers.

Advanced runtime

Import openrtc/runtime only for a runtime adapter or bounded diagnostic tool. Endpoint overrides belong to openrtc/testing and should not enter production bundles.