Advanced Native Runtime
Configure the native bridge and endpoint lifecycle.
Add native ALPN handlers or compose one host-owned Iroh router without duplicating OpenRTC lifecycle
Most applications do not need this page. Use an avenue handle and
channels.register() for ordinary application messages.
A Rust-native host may go further: it can add a custom ALPN handler or compose Iroh Docs, Blobs, Gossip, and OpenRTC on the same endpoint. The extension boundary is deliberately narrow: there must still be one endpoint and one ALPN router, and accepted OpenRTC connections must return to the OpenRTC client that owns admission, peer sessions, replacement, retry, and readiness.
ProtocolPlugin declares the ALPN and supplies its handler. Register it before
binding the endpoint so the ALPN is advertised during negotiation.
use anyhow::Result;
use async_trait::async_trait;
use iroh::{
endpoint::Connection,
protocol::{AcceptError, DynProtocolHandler, ProtocolHandler},
};
use openrtc::protocol_registry::{ProtocolPlugin, RuntimeContext};
#[derive(Debug)]
struct MyHandler;
impl ProtocolHandler for MyHandler {
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
// Handle only example/my-app/1 traffic here.
drop(connection);
Ok(())
}
}
struct MyPlugin;
#[async_trait]
impl ProtocolPlugin for MyPlugin {
fn name(&self) -> &'static str { "my-app" }
fn alpn(&self) -> Option<Vec<u8>> { Some(b"example/my-app/1".to_vec()) }
fn has_router_handler(&self) -> bool { true }
fn router_handler(&self) -> Option<Box<dyn DynProtocolHandler>> {
Some(Box::new(MyHandler))
}
async fn on_runtime_ready(&self, context: RuntimeContext) -> Result<()> {
tracing::debug!(node_id = %context.node_id, "custom protocol ready");
Ok(())
}
}
Use the typed registration method so a session-adjacent plugin cannot accidentally be treated as an ALPN handler:
use openrtc::runtime_manager::RuntimeManager;
use std::sync::Arc;
let runtime = RuntimeManager::new(client.clone());
runtime.register_alpn_protocol(Arc::new(MyPlugin)).await?;
The registry rejects duplicate plugin names and duplicate ALPN values.
register_session_plugin() is the corresponding seam for code that reacts to
an existing OpenRTC session without adding another ALPN.
Registration declares the protocol; it does not create a second router. A
plugin with a router_handler() belongs in the combined-router bootstrap below
and can be added with runtime.apply_registered_protocols(builder).
Products that already own an Iroh protocol suite can install one combined router. Plutonium uses this pattern for its product protocol plus Iroh Docs, Blobs, and Gossip.
The bootstrap order matters:
Client::handle_incoming_connection().use anyhow::Result;
use iroh::{
endpoint::Connection,
protocol::{AcceptError, ProtocolHandler, Router},
};
use openrtc::{client::Client, native_node::PlutoniumProtocol};
use std::sync::Arc;
#[derive(Clone)]
struct OpenRtcHandler(Arc<Client>);
impl ProtocolHandler for OpenRtcHandler {
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
if let Err(error) = self.0.handle_incoming_connection(connection).await {
tracing::warn!(%error, "OpenRTC rejected an incoming connection");
}
Ok(())
}
}
let extra_alpns = vec![
iroh_blobs::ALPN.to_vec(),
iroh_docs::ALPN.to_vec(),
iroh_gossip::ALPN.to_vec(),
];
client
.init_iroh_without_internal_router(secret_key, extra_alpns)
.await?;
let endpoint = client.get_endpoint().await?;
let router = Router::builder(endpoint)
.accept(PlutoniumProtocol::ALPN, OpenRtcHandler(client.clone()))
.accept(iroh_blobs::ALPN, blobs)
.accept(iroh_docs::ALPN, docs)
.accept(iroh_gossip::ALPN, gossip)
.spawn();
The handler values (blobs, docs, and gossip) are created by their owning
Iroh crates. Keep the returned router alive for the lifetime of the host and
shut it down during normal application teardown.
If the host uses ProtocolPlugin objects, include
runtime.protocol_registry().extra_alpns().await in extra_alpns before
endpoint initialization, then call
runtime.apply_registered_protocols(builder) before spawn() instead of
adding those handlers by hand. The base OpenRTC forwarding handler is still
required.
handle_incoming_connection().Browser/WASM cannot install an arbitrary Rust ProtocolHandler, but the
shipping WASM endpoint can enable the upstream Iroh Docs, Blobs, and Gossip
handlers on its existing router. The product supplies one durable
BrowserProtocolStore adapter, normally backed by IndexedDB; OpenRTC
hydrates the Rust actors before exposing them and serves persisted blob ranges
without creating a second endpoint.
import { runtimeFromCapability, type BrowserProtocolStore } from 'openrtc/runtime';
const space = await rtc.spaces.join('design-doc');
const runtime = runtimeFromCapability(space).advanced.runtime;
const protocolStore: BrowserProtocolStore = appIndexedDbIrohStore();
await runtime.initIrohProtocols(protocolStore);
const doc = await runtime.createIrohNamespace();
const receipt = await runtime.putIrohBytes(
doc.namespaceId,
new TextEncoder().encode('title'),
new TextEncoder().encode('OpenRTC architecture'),
);
await protocolStore.acknowledgeOutbox(receipt.operationId);
const ticket = await runtime.shareIrohNamespace(doc.namespaceId, { writable: false });
const rows = await runtime.queryIrohNamespace(doc.namespaceId);
The adapter contract groups identity, author, namespace, signed-entry, outbox,
blob chunk, Bao outboard, staging, delete, and flush operations. Make each
transaction atomic. Use one writer per browser profile (Web Locks plus a
BroadcastChannel handoff is the recommended host policy), fail closed on
quota/corruption/schema mismatch, and call flushIrohProtocols() before a
controlled shutdown. OpenRTC deliberately does not hide those durability
decisions behind an uninspectable database.
Arbitrary custom ALPN handlers remain native-only. Browser application protocols that do not need Docs/Blobs/Gossip should use avenue handles and registered named channels.
Configure the native bridge and endpoint lifecycle.
Choose the public capability handle before extending the runtime.