Ticket mesh

Connect a small peer mesh from one shareable invitation

Planned API: The Rust, browser/WASM, and Tauri surfaces are implemented in source. Use this guide for review and local integration only until the registered cross-runtime scenarios and package release gates pass.

A ticket mesh starts with one invitation. A guest joins it, then OpenRTC connects the admitted guests to one another. The app watches connected peers and sends its own messages or files. It does not select a dialer, build a connection scope, or retry a dropped route.

Issue an invitation

const shareId = crypto.randomUUID();
const mesh = await rtc.tickets.issue(shareId, {
  maxPeers: 8,
  mesh: true,
});

const stopInvite = mesh.watchInvite((invite) => displayQr(invite));
const stop = mesh.peers.watch((peers) => renderPeers(peers));

mesh.invite is the shareable bearer invitation. Put it in a URL fragment if your app creates a link; do not put it in a query string, log, or persistent storage. It is different from the existing install-bound token, which must never be shared.

maxPeers counts everyone, including the issuer. Eight allows the issuer and up to seven guests, subject to the application manifest and server limit. For a two-participant mesh invitation, use maxPeers: 2 and keep mesh: true. Mesh tickets use a memory-only session identity by default. Passing identity: 'session' is equivalent if you prefer to state that policy. Omitting mesh: true preserves the older install-bound ticket capability and does not produce a transferable invitation for tickets.join(). The Rust ticket admission owner enforces this cap on active, authenticated participants, including when peers connect directly without hosted discovery. The Rust implementation renews the expiring invitation through the same mesh owner and projects updates to the handle. Use mesh.watchInvite() for a current QR or link; a previously copied bearer remains subject to its original expiry.

Join from another device

const mesh = await rtc.tickets.join(invite);
const issuerNode = await mesh.issuerNode();
const stop = mesh.peers.watch((peers) => renderPeers(peers));

// A guest can share the current issuer invitation, including renewals.
const stopInvite = mesh.watchInvite((current) => displayQr(current));

stopInvite();
stop();
await mesh.close();

close() resolves after the runtime owner confirms teardown. If a transient native bridge error rejects the call, the handle remains open; retry mesh.close() on that same handle rather than issuing or joining again. Concurrent close calls share one in-flight teardown and cannot create or release a second owner early. Starting and closing are also serialized per mesh ID. Await issue, join, or close before starting another operation for that ID.

Errors

Ticket mesh failures use the normal RTCError surface. Check error.code instead of matching message text:

CodeMeaningRetry the same operation?
admission/ticket-invite-invalidThe invitation is malformed, expired, unscoped, or is not a mesh invitation.No; obtain a current invitation.
admission/ticket-mesh-invalid-optionsmaxPeers is outside 2..8, is not an integer, or conflicts with mesh options.No; correct the options.
admission/ticket-mesh-capacity-exceededThe Rust admission owner has reached the invitation's participant cap.No; a participant must leave or the issuer must create a new session.
admission/ticket-mesh-already-activeThis runtime already owns or is closing the same ticket ID.No; reuse or await the existing handle.
coordination/ticket-mesh-unavailableThe native/WASM owner or its bridge could not complete the operation.Yes when retryable is true; retry the same handle or logical operation.

join connects the guest to the issuer. With mesh: true, OpenRTC then connects the guest to other admitted guests. peers.watch reports only authenticated, ready peers in this ticket session. A roster entry alone is never reported as connected.

Rust crate surface

The Rust crate API uses the same invitation and one shared runtime. Its native ticket-mesh handle owns issue, join, peer observation, and close; Tauri calls into that owner rather than creating another connection loop.

use openrtc::{TicketMesh, TicketMeshOptions};

// Issuer runtime
let mesh = TicketMesh::issue(client.clone(), share_id, TicketMeshOptions {
    max_peers: 8,
}).await?;
let invite = mesh.invite();
let mut peers = mesh.watch_peers();
let issuer_node = mesh.issuer_node()?;
// Keep `mesh` alive while the invitation is active, then:
mesh.close().await;
use openrtc::TicketMesh;

// Guest runtime
let mesh = TicketMesh::join(client.clone(), &invite).await?;
let mut peers = mesh.watch_peers();
mesh.close().await;

client is an existing shared Rust peer runtime with an initialized endpoint. The native handle also has watch_invite() for renewal. Browser/WASM and Tauri use Rust owners governed by the same admission, topology, retry, liveness, and close policy.

Rust results retain a typed TicketMeshError inside anyhow::Error. Downcast it and switch on TicketMeshError::code() / TicketMeshErrorCode; the as_str() values match the TypeScript codes in the table above. Tauri and browser/WASM project those same codes as RTCError.code.

Who owns the connection

ConcernOwner
Invitation admission, member limit, peer selection, route retry, connection health, and retirementOpenRTC's shared Rust core, exposed by the Rust crate and projected to Tauri and browser/WASM
Issuer roster deliveryOpenRTC over protected peer channels
File choice, recipient labels, product permissions, and transfer historyYour app

The issuer is the roster authority; it is not a second transport owner. OpenRTC will fence roster revisions, recover after packet drops, and remove a guest only after its connection health decision. Closing the issuer ends the invitation and mesh. Closing a guest leaves that guest only. There is no automatic issuer election. If the issuer disappears without an explicit close, the native and WASM actors keep retrying with bounded backoff. A prolonged outage that outlasts the invitation's bearer may require a fresh invitation. An explicit guest leave or issuer close uses the authenticated ticket route, retries its notice within a bounded window, and waits for the matching peer acknowledgement before local revocation. If the final exchange cannot cross a failed route, the existing health and lease policy still converges without an application heartbeat or a second cleanup owner.

This design adds no separate hosted membership service. Existing capability issuance still applies, and a peer route may use an allowed relay when a direct connection cannot be established. Every file sent to several guests still needs delivery to each guest; an app may present those deliveries as one grouped transfer.

See Tickets for direct, manually connected tickets and Rooms for managed room membership and larger groups.