# Using iroh in WebAssembly and Browser Environments: A Complete Guide

> Learn to use iroh in WebAssembly and browsers. This guide shows how to enable direct P2P connections without native UDP. Explore browser-compatible async runtimes and relay-only networking.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: tutorial
- Published: 2026-07-11

---

**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`](https://github.com/n0-computer/iroh/blob/main/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::Sleep` wrapped in a `web::Timer` struct instead of Tokio timers
- **Transports**: The `TransportConfig` enum removes the `Ip` variant in Wasm builds, restricting connections to relay and custom transports only

## Runtime Implementation in [`iroh/src/runtime.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/runtime.rs)

The core adaptation lives in [`iroh/src/runtime.rs`](https://github.com/n0-computer/iroh/blob/main/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::Runtime` with `new_timer` delegating to `noq::TokioRuntime`
- Uses `TaskTracker` and 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::Timer` wrapping `n0_future::time::Sleep` for QUIC timer compatibility
- Provides no-op `shutdown` and `abort` methods 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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs). When targeting WebAssembly, the codebase eliminates IP transport capabilities while preserving relay functionality:

- The `TransportConfig::Ip` variant is guarded by `#[cfg(not(wasm_browser))]`
- The `LocalAddrsWatch` type 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`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml) in the iroh crate declares WebAssembly-specific dependencies under conditional targets:

```toml
[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:

1. **Build the Wasm artifact**:

```bash
cargo build --release --target wasm32-unknown-unknown

```

This generates `target/wasm32-unknown-unknown/release/iroh.wasm`.

2. **Generate JavaScript bindings**:

```bash
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:

```rust
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 `Runtime` automatically uses `web::Timer` and `wasm_bindgen_futures::spawn_local` internally
- The `bind()` method succeeds because the transport layer excludes IP variants in Wasm builds
- Error handling converts Rust errors to JavaScript-compatible `JsValue` types

### Connecting from JavaScript

After generating bindings with `wasm-bindgen`, consume the library in a browser script:

```javascript
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`](https://github.com/n0-computer/iroh/blob/main/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:

```bash
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.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/runtime.rs) adapts task spawning to `wasm_bindgen_futures::spawn_local` and timers to `n0_future::time::Sleep` in browser environments
- **Transports** are restricted to relay and custom variants in [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs) because browsers cannot open raw UDP sockets
- Building requires targeting `wasm32-unknown-unknown` and using `wasm-bindgen` to 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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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.