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:
| Code | Meaning | Retry the same operation? |
|---|---|---|
admission/ticket-invite-invalid | The invitation is malformed, expired, unscoped, or is not a mesh invitation. | No; obtain a current invitation. |
admission/ticket-mesh-invalid-options | maxPeers is outside 2..8, is not an integer, or conflicts with mesh options. | No; correct the options. |
admission/ticket-mesh-capacity-exceeded | The 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-active | This runtime already owns or is closing the same ticket ID. | No; reuse or await the existing handle. |
coordination/ticket-mesh-unavailable | The 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
| Concern | Owner |
|---|---|
| Invitation admission, member limit, peer selection, route retry, connection health, and retirement | OpenRTC's shared Rust core, exposed by the Rust crate and projected to Tauri and browser/WASM |
| Issuer roster delivery | OpenRTC over protected peer channels |
| File choice, recipient labels, product permissions, and transfer history | Your 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.