Avenues
Choose the smallest OpenRTC scope for devices, spaces, rooms, or ticket sessions
An avenue is the one place where a set of peers is allowed to find each other. Activating an avenue returns a handle that owns that avenue's presence, connections, diagnostics, refresh, and cleanup.
The practical rule is simple: create one lazy client, activate only the avenue your feature needs, and close that handle when the feature ends.
| If you are connecting... | Use | What remains after everyone leaves? |
|---|---|---|
| One signed-in user's own installations | devices | Enrolled devices remain known; live presence does not |
| Whoever is here right now | space | Nothing |
| People intentionally in one session | room | Nothing by default; durable membership is opt-in |
| Peers in one explicit, bounded handoff | ticket | Nothing |
Constructors are side-effect free. The awaited start, join, or issue
call obtains one scoped capability; the handle then owns the live avenue.
Common TypeScript setup
import { OpenRTC } from 'openrtc';
const rtc = OpenRTC({
apiKey: import.meta.env.VITE_OPENRTC_API_KEY,
});
In a Tauri WebView, import OpenRTC from openrtc/native and pass the
openrtc-tauri bridge. It returns the same four namespaces without loading the
WASM runtime, so shared application code does not need separate browser and
desktop avenue APIs.
Common Rust setup
Pure Rust hosts keep one per-install Ed25519 signer in platform-secure storage. The high-level control plane is lazy just like the TypeScript client:
use openrtc::native::{
ControlPlane,
DeviceSigner,
};
use std::sync::Arc;
const API_KEY: &str = "pk_live_0000000000000000000000000000000000000000";
// Supplied by your app; the private key never enters OpenRTC.
let signer: Arc<dyn DeviceSigner> = app_secure_signer();
let rtc = ControlPlane::anonymous(API_KEY, signer.clone())?;
Each Rust activation below returns a handle with signaling() and close().
Install signaling() once in the openrtc::client::Client owned by that
feature. Do not install multiple avenue handles into one client or build a
second reconnect/presence loop around it.
1. Devices: "my other devices"
Choose devices when the peers are installations owned by the same signed-in
product user: a laptop finding a phone, or a desktop finding the user's second
computer.
- Identity: authenticated and app-scoped
- Discovery: durable enrolled-device roster plus live status
- Connection policy: online devices can auto-connect
- Best for: personal device mesh, sync, handoff between owned devices
- Avoid for: public lobbies or temporary collaborators
TypeScript
const devices = await rtc.devices.start({
auth: myAuthProvider,
autoConnect: 'online',
profile: { name: 'Bryant\'s MacBook', platform: 'macos' },
});
const stopWatching = devices.watch((knownDevices) => {
renderDevicePicker(knownDevices);
});
const statuses = await devices.listWithStatus();
// Later
stopWatching();
await devices.close();
Offline devices remain in the enrolled roster until they are revoked or age
out under retention policy. autoConnect: 'online' never dials an offline
inventory record merely because it is still known.
Rust
use openrtc::{
Client,
native::{
AssertionProvider,
CertificateStore,
ControlPlane,
DeviceOptions,
},
};
let assertions: Arc<dyn AssertionProvider> = app_assertions();
let certificates: Arc<dyn CertificateStore> = app_certificate_store();
let control = ControlPlane::new(
API_KEY,
assertions,
signer,
certificates,
)?;
let devices = control.devices(
installation_id,
"desktop",
DeviceOptions::default(),
).await?;
let client = Client::builder(
API_KEY.to_owned(),
devices.identity_credential_provider(),
)?
.signaling_backend(devices.signaling())
.build();
// Use `client` for this device avenue, then retire the owner explicitly.
devices.close().await;
The app owns its assertion provider, secure signer, and protected certificate store. OpenRTC owns assertion exchange, enrollment, grant refresh, gateway presence, and reconnect decisions.
2. Spaces: "whoever is here now"
Choose spaces for live-only presence where joining the same name is enough:
shared cursors, ambient portfolio visitors, a prototype lobby, or collaborative
art.
- Identity: install capability, or a new session identity
- Discovery: live roster only
- Connection policy: peers auto-connect while present
- Best for: latest-state UI, demos, lightweight presence
- Avoid for: durable ownership or membership
TypeScript
const cursors = await rtc.spaces.join('portfolio-cursors', {
payload: 'latest-state',
});
const stopWatching = cursors.peers.watch((peers) => {
renderRemoteCursors(peers);
});
// Later
stopWatching();
await cursors.leave();
access: 'capability' is the default. Add identity: 'session' when each
OpenRTC client run needs a new identity. State that can be superseded, such as pointer position, can opt into
payload: 'latest-state'; ordinary reliable delivery is the default.
Spaces admit at most 50 members in this release. The ordinary full-mesh path
remains the small-group default. A 9–50 member latest-state space must also use
the reviewed advancedFanout: true compatibility option; unsupported app
manifests fail closed instead of building an unbounded mesh. Rooms provide the
newer architecture: 'auto' surface when automatic policy is required.
Rust
use openrtc::{
Client,
native::CapabilityOptions,
};
let space = rtc.join_space(
"portfolio-cursors",
"desktop",
CapabilityOptions::default(),
).await?;
let client = Client::builder(
API_KEY.to_owned(),
Box::new(|| None),
)?
.signaling_backend(space.signaling())
.build();
// Use `client` for this live space.
space.close().await;
3. Rooms: "we intentionally joined this session"
Choose rooms when the product has an explicit session identifier: a call,
game match, support session, or collaborative document.
- Identity: capability by default; authenticated when product policy needs it
- Discovery: members of this room
- Membership: ephemeral by default
- Best for: intentional groups with a meaningful room ID
- Avoid for: a user's general device roster
TypeScript
const room = await rtc.rooms.join('match-123');
const stopWatching = room.peers.watch((peers) => {
renderPlayers(peers);
});
// Later
stopWatching();
await room.leave();
Only request authenticated, durable membership when the product actually needs it:
const team = await rtc.rooms.join('team-roadmap', {
access: 'authenticated',
membership: 'durable',
auth: myAuthProvider,
});
Durable membership is portal-enabled and adds durable control-plane work. It is not a reliability switch for an ordinary live session.
Rooms default to automatic architecture selection:
const room = await rtc.rooms.join('town-hall', {
maxPeers: 50,
architecture: 'auto',
});
await room.channel('chat').send({ text: 'hello' });
Small rooms remain fully connected. From 9–50 members, eligible latest-state rooms can use two neighbors in each direction. At 50 members that is 100 physical overlay edges instead of 1,225 full-mesh edges. Named-channel envelopes are deduplicated in a bounded 4,096-message cache.
When membership changes, the gateway can keep displaced active routes as a small overlap set. The Rust connection owner retains that overlap only until every new active connection has completed transport and admission checks, then retires it. First-time and steady topologies do not dial speculative backups, and apps never manage this overlap.
The gateway remains membership authority and the Rust peer-session actor still
owns every dial, retry, replacement, and settlement transition. OpenRTC does
not start a second raw iroh-gossip dialer. Rust signs canonical envelope bytes
through the existing non-exportable device signer and verifies the author,
membership revision, payload hash, lifetime, and message ID before delivery.
The peerId delivered to sparse channel observers is the authenticated durable
device ID; private Iroh node IDs remain route metadata and are exposed only
when the current membership revision explicitly binds that device to a route.
The mutable hop counter is an honest-route loop bound, not proof against a
malicious intermediary. Per-node dedupe and the reviewed 50-member ceiling are
the amplification boundary until scale budgets are measured.
state() is unavailable in sparse rooms; publish application-owned monotonic
latest-state revisions over a named channel instead. onConnection() exposes
only physical neighbors. Targeted delivery to a non-neighbor fails closed and
must first obtain a direct authorized route.
See Adaptive large rooms for the room-wide snapshot, planning quote, transition rules, fixed-mode limits, and native Rust API.
Rust
let room = rtc.join_room(
"match-123",
"desktop",
CapabilityOptions::default(),
).await?;
let client = Client::builder(
API_KEY.to_owned(),
Box::new(|| None),
)?
.signaling_backend(room.signaling())
.build();
// Use `client` for this room.
room.close().await;
The pure-Rust high-level control plane currently exposes ephemeral capability rooms. Authenticated durable membership remains a TypeScript/control-plane integration surface in this RC.
4. Tickets: "this one explicit handoff"
Choose tickets for a bounded session that should not create a durable roster:
pairing, a one-time handoff, or a narrowly scoped support exchange.
- Identity: ephemeral, device-bound capability
- Discovery: no durable roster; only the active ticket session
- Connection policy: manual by default
- Best for: explicit, bounded peer selection
- Avoid for: product authorization or long-lived group membership
TypeScript
Both participants activate the same high-entropy, app-generated ID. Ticket avenues do not auto-connect, so the app selects the live peer explicitly:
const handoff = await rtc.tickets.issue(handoffId, { maxPeers: 2 });
let peerSelected = false;
const stopWatching = handoff.peers.watch(async (peers) => {
if (peerSelected) return;
const peer = peers.find((candidate) => candidate.online && candidate.ticket);
if (!peer?.ticket) return;
peerSelected = true;
try {
await handoff.peers.connect({
deviceId: peer.deviceId,
ticket: peer.ticket,
scope: `ticket:${handoff.id}`,
});
} catch (error) {
peerSelected = false;
console.error(error);
}
});
// Later
stopWatching();
await handoff.close();
Share handoffId through an application-controlled authenticated channel or a
QR payload designed by your product. The current root API does not expose
tickets.accept() or server-side ticket.revoke(). handle.token is the
issuing installation's opaque OpenRTC capability: do not display, log, or hand
it to another installation as a bearer invitation.
Rust
let ticket = rtc.issue_ticket(
handoff_id,
"desktop",
CapabilityOptions {
max_peers: Some(1),
..Default::default()
},
).await?;
let client = Client::builder(
API_KEY.to_owned(),
Box::new(|| None),
)?
.signaling_backend(ticket.signaling())
.build();
// Observe/select the peer through `client`; ticket avenues do not auto-dial.
ticket.close().await;
Like the TypeScript surface, ControlPlane::issue_ticket() activates
the scoped session but does not return a transferable invitation token.
For one transferable invitation and automatic connections among a small group,
use tickets.issue(id, { mesh: true }) / tickets.join(invite) or the Rust
TicketMesh handle described in Ticket mesh.
Defaults first, controls second
Most applications should omit options until a requirement appears:
await rtc.spaces.join('presence');
await rtc.rooms.join('match-123');
The focused controls are:
| Control | Use it when... |
|---|---|
payload: 'latest-state' | older queued state is useless, such as cursor movement |
access: 'authenticated' | room policy depends on the product user |
membership: 'durable' | membership must survive everyone leaving |
maxPeers | the feature wants a lower ceiling than the app manifest |
architecture: 'auto' | a room may grow and OpenRTC should choose its safe eligible shape |
architecture: 'mesh' | 'sparse' | the entire room needs a reviewed fixed mode; advancedFanout is deprecated |
transports | the app deliberately opts into relay, WebRTC, MoQ, BLE, or privacy policy |
Client options can lower credit use or capability; they cannot raise server budget, manifest, fan-out, or platform limits.
Application protocols
An avenue answers who may discover whom. A channel answers what those
peers exchange. Register application channels once before activating the
handle, then use the handle's scoped channels view:
const unregister = rtc.channels.register({
id: 'cursor.position.v1',
kind: 'sync',
ownership: 'shared',
peerModel: 'anonymous-ticket',
routing: 'stream-envelope',
readiness: 'settled-peer',
delivery: 'latest-state',
});
const space = await rtc.spaces.join('portfolio-cursors', {
payload: 'latest-state',
});
// Open streams through space.channels.
await space.leave();
unregister();
Custom native ALPNs and a host-owned Iroh router are available as an advanced extension. They extend the shared endpoint; they do not replace OpenRTC's avenue, admission, retry, or connection-lifecycle owners.