openrtc
v2.9.7
Published
A Rust-first realtime runtime for browser and native apps, exposed through TypeScript and WASM.
Maintainers
Readme
OpenRTC
OpenRTC is a provider-neutral peer coordination and transport SDK for browsers, desktop applications, and mobile applications. The 2.0 API is capability based: constructing a client is local and side-effect free, and network work starts only when the application activates a device, space, room, or ticket handle.
The core is message-agnostic. Named channels are the application protocol extension boundary; they do not replace the incoming router or authorize a parallel lifecycle owner.
Install
pnpm add openrtcFast prototype
Only a public API key is required for a bounded ephemeral space:
import { OpenRTC } from 'openrtc';
const rtc = OpenRTC({ apiKey: import.meta.env.VITE_OPENRTC_API_KEY });
const cursors = await rtc.spaces.join('portfolio-cursors', {
payload: 'latest-state',
});
const stop = cursors.peers.watch((peers) => console.log(peers));
// Later
stop();
await cursors.leave();
await rtc.close();The constructor performs no network request, storage access, authentication, attestation, timer creation, WASM load, or transport startup.
Authenticated devices
Your application owns login and supplies a short-lived assertion from its registered OIDC/JWKS provider. OpenRTC validates it, binds the installation's device key, and issues scoped gateway grants.
const auth = {
getAssertion: ({ forceRefresh }: { forceRefresh: boolean }) =>
fetch('/api/openrtc/assertion', {
method: 'POST',
headers: { 'X-Force-Refresh': String(forceRefresh) },
}).then((response) => response.text()),
};
const rtc = OpenRTC({
apiKey: import.meta.env.VITE_OPENRTC_API_KEY,
auth,
});
const devices = await rtc.devices.start({
auth,
autoConnect: 'online',
// Optional. Browser clients infer labels such as "Chrome on Android";
// native clients should pass the host-reported OS and user-visible name.
profile: { name: "Bryant's iPhone", platform: 'ios' },
});profile is presentation metadata, not device identity. OpenRTC continues to
bind enrollment and limits to its stable installation ID and device key. A
changed profile updates the existing enrollment once; it does not enroll or
bill a second device. User-assigned hardware names are not exposed by every
browser or mobile OS, so privacy-safe defaults identify the available
environment (for example iPhone, Chrome on iPhone, or Chrome on Android).
Consumer apps own login UX, product authorization, IdP registration, and platform-attestation setup. OpenRTC owns assertion validation, device binding, grant issuance and refresh, revocation, budgets, metering, and abuse controls.
Rooms
Rooms default to ephemeral membership:
const room = await rtc.rooms.join('match-123');Durable membership is an advanced capability and must be enabled in the developer portal and requested by the application:
const team = await rtc.rooms.join('team', {
access: 'authenticated',
membership: 'durable',
auth,
});Every long-lived capability has close(); spaces and rooms also have
leave(). One handle activates exactly one avenue.
Await close() before reusing an avenue. If it rejects, retry close() on the
same handle or root client; OpenRTC retains that exact owner until cleanup
succeeds. Concurrent calls share the pending cleanup. A handle's closed flag
denies product access but does not prove resource retirement; a failed release
cannot be replaced by another activation of the same avenue. Consumers should
surface the cleanup failure while preserving their local documents, without
adding connection recovery or cleanup timers.
Developer service nodes
openrtc/service-node exposes a preview WebSocket-server-like application
surface for browser/WASM. Browser code supplies a callback
that obtains a short-lived authority grant from the developer backend; it must
never receive a developer sk_* key. Trusted Node/server code can mint that
grant with openrtc/server.
import { ServiceNode } from 'openrtc/service-node';
const service = new ServiceNode({
client: rtc,
apiKey,
serviceId: 'world-simulation',
generation: 1,
shardIds: ['zone-a'],
deviceId: await rtc.devices.localId(),
deviceSigner,
assignmentSigner,
grantIssuer: (request) => fetch('/api/openrtc/authority-grant', {
method: 'POST',
body: JSON.stringify(request),
}).then((response) => response.json()),
});
service.onConnection(({ connection }) => console.log(connection.id));
await service.listen('world-42');Trusted Node.js authorities use the dedicated Rust host through openrtc/node;
no application IPC bridge or native addon is required:
import { ServiceNode } from 'openrtc/node';
const service = new ServiceNode({
apiKey: process.env.OPENRTC_API_KEY!,
secretKey: process.env.OPENRTC_SECRET_KEY!,
serviceId: 'world-simulation',
generation: 1,
shardIds: ['zone-a'],
deviceId: 'world-simulation-1',
stateDirectory: '/var/lib/world-simulation/openrtc', // private and persistent
});
service.onConnection(({ connection }) => {
connection.onMessage((message) => console.log(connection.peerId, message));
});
await service.listen('world-42');maxPeers counts everyone in the room, including the service. Node defaults to
eight; set a value from 1 through 50 explicitly when needed. Your app's approved
limit and available credits still apply, so a larger request can be denied.
Browser and standalone Rust grant requests may omit the value to use app policy.
These limits are not a promise of 50 simultaneous server connections or a
replacement for the remaining authority acceptance tests.
The released package must contain the signed native artifact for the current
process.platform/process.arch; a missing artifact fails closed. A reviewed
source checkout can instead opt into buildFromSource: true with an absolute
sourceRoot. The developer secret is sent only over the child's stdin, and
the Node layer only demultiplexes bounded application messages and authority
assignments. The Rust peer-session actor remains the sole lifecycle owner.
connection.peerId is the admitted remote endpoint ID, not an app account or
durable device ID. Room access alone does not grant those identities. Keep
stateDirectory across process restarts to retain the host's private keys.
Await service.close() before reusing that directory; it waits for the host
process to finish, not just acknowledge shutdown.
JSON and string messages use the same room-scoped envelope as browser
Connection.send; onMessage receives the application value, not the envelope.
Messages explicitly addressed to another room are discarded. Node byte payloads
(Uint8Array/ArrayBuffer) retain the lower-level native byte contract; use
JSON values for portable browser/Node application messages.
This preview Node facade intentionally owns one room and exposes only
message-oriented send/onMessage/onClose plus assignment publication; it
does not yet claim the browser facade's streams, media, or full Room
namespaces. Browser resolution of openrtc/node fails closed; browser code
uses WASM plus delegated grants through openrtc/service-node. Native Rust
uses ControlPlane::join_authority_room directly and does not require
JavaScript. Its RoomAuthority::compose_client(builder) method binds the
dedicated service-room client to the existing Rust peer-session owner before
endpoint initialization. Native hosts publish a room-scoped compound ticket;
attaching the signaling backend alone is not service startup.
The authority surface remains preview-only until signed prebuilt artifacts and source-bound browser, Node, and native Rust authority E2E lanes pass.
Optional trust adapters
Attestation is evaluated at device enrollment, key rotation, recovery, or risk escalation—not as a connection heartbeat.
import { AppCheckProvider } from 'openrtc-trust-firebase-app-check';
const rtc = OpenRTC({
apiKey,
auth,
trust: {
attestation: new AppCheckProvider({ appCheck }),
},
});Managed Firebase App Check, Apple App Attest, and Play Integrity are optional packages. A consumer backend may instead validate platform evidence and issue the registered OIDC assertion; that is the canonical production integration.
Usage controls
const rtc = OpenRTC({
apiKey,
usage: {
warnAtSessionCreditsUsd: 0.25,
maxSessionCreditsUsd: 0.50,
onUsage: console.log,
onWarning: console.warn,
},
});
rtc.usage.estimate({ operation: 'coordination.connection.open', units: 3 });The client ceiling is an additional local guard. Server budgets and developer manifest limits remain authoritative.
Native and WASM
import { OpenRTC } from 'openrtc';
import { createBridge } from 'openrtc-tauri/ipc';
const rtc = OpenRTC.native({ apiKey, auth }, createBridge());Native hosts keep one Ed25519 installation key in host secure storage. Browsers use a non-extractable WebCrypto key in IndexedDB when supported. The Rust and WASM clients use the same provider-neutral avenue-grant contract as TypeScript.
Low-level runtime access is intentionally isolated in openrtc/runtime for
transport authors, migration harnesses, and diagnostics. Test endpoint
overrides live only in openrtc/testing.
Extensions
y-openrtcadapts one activated room to Yjs.openrtc-netcodeowns matches, replicated state, latest-state transforms, chat/control, and optional voice helpers.openrtc-file-transferowns bounded, acknowledged, resumable file transfer.
Relay, durable membership, managed attestation, MoQ, and BLE require both
portal enablement and explicit runtime opt-in. Avenues default to 8 peers.
Reviewed non-room avenues may request 9-12 peers. Rooms default to
architecture: 'auto' and payload: 'reliable'. Small rooms use a direct
mesh. A room may declare payload: 'latest-state' so auto can use the reviewed
four-neighbor sparse graph through 50 members. Reliable rooms above the safe
mesh limit require
the explicitly enabled, content-blind managed path. advancedFanout: true is a
one-release alias for fixed sparse latest-state delivery. state() remains
unavailable in sparse rooms; use a named channel with application revisions.
Passive connection metrics
const stop = ticket.diagnostics.watchConnectionMetrics((connections) => {
for (const connection of connections) {
console.log(connection.latencyMs, connection.activeTransport, connection.carrier);
}
});
// Stop observing when the view closes. Ticket peers remain SDK-owned.
stop();OpenRTC automatically samples local Rust/WASM connection diagnostics every two
seconds while observed. All observers of a runtime share one sampler. It sends
no pings and makes no coordination requests. Ticket observers see only that
ticket's scope. Iroh supplies passive selected-path RTT and the observed carrier
or path (webrtc, moq, iroh-quic, iroh-lan, iroh-relay, or ble).
A missing, unadmitted, replaced, or retired path has null measurements. Samples
carry the existing transport stable ID, transport/route generations, and
metricsSampledAtMs. These read-only diagnostics do not determine readiness.
