Using iroh in WebAssembly and Browser Environments: A Complete Guide
iroh compiles to WebAssembly using conditional compilation to provide browser-compatible async runtimes and relay-only networking, enabling direct peer-to-peer connections in browsers without native UDP sockets.
The iroh library from n0-computer/iroh is architected to run across native platforms and WebAssembly (Wasm) environments using a single Rust codebase. By leveraging conditional compilation attributes, iroh adapts its runtime and transport layers to browser constraints while maintaining API compatibility. This guide explains how to build, configure, and deploy iroh in WebAssembly and browser environments based on the actual implementation in the source code.
Conditional Compilation Architecture
iroh uses #[cfg(wasm_browser)] and #[cfg(not(wasm_browser))] attributes to isolate platform-specific code paths. This approach allows the crate to declare a single crate-type = ["lib", "cdylib"] in Cargo.toml while supporting both native targets and wasm32-unknown-unknown.
The conditional compilation affects three critical subsystems:
- Runtime: Native builds use Tokio-based task trackers, while Wasm builds delegate to
wasm_bindgen_futures::spawn_local - Timers: Browser implementations use
n0_future::time::Sleepwrapped in aweb::Timerstruct instead of Tokio timers - Transports: The
TransportConfigenum removes theIpvariant in Wasm builds, restricting connections to relay and custom transports only
Runtime Implementation in iroh/src/runtime.rs
The core adaptation lives in iroh/src/runtime.rs, where the Runtime struct implements the noq::Runtime trait. When compiled for wasm_browser, this file provides a lightweight runtime that differs significantly from its native counterpart:
Native Runtime (#[cfg(not(wasm_browser))]):
- Implements
noq::Runtimewithnew_timerdelegating tonoq::TokioRuntime - Uses
TaskTrackerand cancellation tokens for async task management - Provides full shutdown and abort capabilities
Browser Runtime (#[cfg(wasm_browser)]):
- Spawns tasks using
wasm_bindgen_futures::spawn_local, scheduling futures on the JavaScript micro-task queue - Implements
web::Timerwrappingn0_future::time::Sleepfor QUIC timer compatibility - Provides no-op
shutdownandabortmethods since browsers lack process termination semantics
This design ensures that async operations use the browser's event loop without requiring a separate thread pool, which is impossible in Wasm's single-threaded environment.
Transport Layer Limitations in iroh/src/socket/transports.rs
Browser security models prohibit raw UDP socket access, forcing iroh to modify its transport layer in iroh/src/socket/transports.rs. When targeting WebAssembly, the codebase eliminates IP transport capabilities while preserving relay functionality:
- The
TransportConfig::Ipvariant is guarded by#[cfg(not(wasm_browser))] - The
LocalAddrsWatchtype monitors only Relay and Custom transports - Direct peer-to-peer connections rely on iroh's relay network for NAT traversal
This restriction means Wasm builds operate in relay-only mode, which is sufficient for most browser use cases. The relay network handles NAT traversal and provides fallback connectivity when direct UDP is unavailable.
Dependencies and Cargo Configuration
The Cargo.toml in the iroh crate declares WebAssembly-specific dependencies under conditional targets:
[dependencies]
# Native dependencies
tokio = { version = "1", features = ["full"] }
tokio-util = "0.7"
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
wasm-bindgen-futures = "0.4"
time = { version = "0.3", features = ["wasm-bindgen"] }
getrandom = { version = "0.2", features = ["wasm_js"] }
The wasm-bindgen feature on the time crate ensures timer compatibility, while getrandom with the wasm_js feature provides cryptographically secure random number generation required for QUIC handshakes.
Building for WebAssembly
To compile iroh for browser environments, follow the standard wasm-bindgen workflow:
- Build the Wasm artifact:
cargo build --release --target wasm32-unknown-unknown
This generates target/wasm32-unknown-unknown/release/iroh.wasm.
- Generate JavaScript bindings:
wasm-bindgen target/wasm32-unknown-unknown/release/iroh.wasm \
--out-dir ./pkg --target web
The --target web flag produces ES6 modules suitable for modern bundlers or direct browser import.
Code Examples
Initializing an Endpoint in the Browser
The following Rust code demonstrates creating a relay-only endpoint suitable for WebAssembly compilation. Note the absence of IP transport configuration and the use of wasm_bindgen for JavaScript interoperability:
use iroh::{Endpoint, address_lookup::pkarr::PkarrResolver};
use iroh::endpoint::presets;
use wasm_bindgen::prelude::*;
const ECHO_ALPN: &[u8] = b"echo";
#[wasm_bindgen]
pub async fn start_endpoint() -> Result<JsValue, JsValue> {
// Relay-only endpoint (IP transport disabled in Wasm)
let endpoint = Endpoint::builder(presets::N0)
.relay_mode(iroh::RelayMode::Staging)
.bind()
.await
.map_err(|e| e.to_string())?;
// Resolve endpoint ID via PKARR DNS
let resolver = PkarrResolver::n0_dns().build(endpoint.tls_config().clone());
let stream = resolver
.resolve(endpoint.id())
.await
.ok_or_else(|| "no DNS records".to_string())?;
Ok(JsValue::from_str(&endpoint.id().to_string()))
}
Key implementation details:
- The
Runtimeautomatically usesweb::Timerandwasm_bindgen_futures::spawn_localinternally - The
bind()method succeeds because the transport layer excludes IP variants in Wasm builds - Error handling converts Rust errors to JavaScript-compatible
JsValuetypes
Connecting from JavaScript
After generating bindings with wasm-bindgen, consume the library in a browser script:
import init, { start_endpoint, connect_to } from './iroh.js';
async function runDemo() {
await init();
const myId = await start_endpoint();
console.log('My EndpointId:', myId);
// Connect to remote peer
const remoteId = "a1b2c3d4...";
const conn = await connect_to(remoteId, "echo");
const { send, recv } = await conn.openBi();
await send.writeAll(new TextEncoder().encode("Hello from Wasm!"));
await send.finish();
const response = await recv.readToEnd(1024);
console.log(new TextDecoder().decode(response));
}
runDemo();
The connect_to function (defined in Rust) calls endpoint.connect(remote_id, alpn).await, with all async operations scheduled via the browser's event loop through wasm_bindgen_futures::spawn_local.
Testing and Integration
The repository includes Wasm-specific integration tests in iroh/tests/integration.rs that use wasm-bindgen-test for browser validation. These tests exercise the same high-level API (Endpoint::builder(...).bind().await) but switch to wasm_tracing for logging when compiled for the browser target.
To run browser tests:
wasm-pack test --headless --firefox
Summary
- iroh uses
#[cfg(wasm_browser)]conditional compilation to support WebAssembly without breaking native functionality - The runtime in
iroh/src/runtime.rsadapts task spawning towasm_bindgen_futures::spawn_localand timers ton0_future::time::Sleepin browser environments - Transports are restricted to relay and custom variants in
iroh/src/socket/transports.rsbecause browsers cannot open raw UDP sockets - Building requires targeting
wasm32-unknown-unknownand usingwasm-bindgento generate JavaScript bindings - The high-level API remains identical between native and Wasm, with
Endpoint::bind()automatically selecting the appropriate runtime and transport configuration
Frequently Asked Questions
Why can't I use direct IP connections in the browser?
Browsers sandbox network access and prohibit raw UDP socket creation for security reasons. As implemented in iroh/src/socket/transports.rs, the TransportConfig::Ip variant is compiled out in Wasm builds, leaving only relay and custom transports. This forces connections through iroh's relay network, which handles NAT traversal and message forwarding on behalf of browser clients.
How do timers work differently in Wasm versus native platforms?
Native iroh builds use Tokio's timer wheel implementation, but Wasm lacks access to system timer APIs. In iroh/src/runtime.rs, the browser runtime defines web::Timer which wraps n0_future::time::Sleep to provide async/.await compatible delays. This ensures QUIC protocol timers and connection timeouts function correctly within the browser's single-threaded event loop.
Can I use custom transports in browser environments?
Yes. While the Ip transport is unavailable in Wasm, the TransportConfig enum retains the Custom variant in iroh/src/socket/transports.rs. You can implement custom transport logic using WebRTC data channels, WebTransport, or other browser-supported networking primitives, provided you implement the required traits and handle the JavaScript interoperability yourself.
What is the performance impact of relay-only mode?
Relay-only connections introduce additional latency compared to direct UDP connections because traffic routes through relay servers. However, iroh's relay protocol is optimized for low latency, and the browser constraint makes this trade-off necessary. The LocalAddrsWatch type in transport configuration ensures the endpoint automatically manages relay connections without API changes, maintaining the same async interface as native builds.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →