---
title: Browser quickstart
description: Install @minip2p/web, dial a native node over WebTransport, and measure a Ping RTT.
---

`@minip2p/web` runs minip2p in the browser as WebAssembly. It dials native minip2p nodes over libp2p WebTransport and has the same API as `@minip2p/node`. Chrome and Firefox are supported; Safari 26.4+ is best-effort.

## Install

```bash
npm install @minip2p/web
```

The default entry loads its `.wasm` with top-level `await`, resolved from `import.meta.url`. Bundlers emit it as an asset; give them an `es2022` target. With Vite, also keep the package out of dependency pre-bundling:

```ts title="vite.config.ts"
import { defineConfig } from "vite";

export default defineConfig({
  build: { target: "es2022" },
  optimizeDeps: { exclude: ["@minip2p/web"] },
});
```

To serve the `.wasm` yourself (a CDN, a strict CSP), import `@minip2p/web/manual` instead. It loads nothing at import; await `initMinip2p({ wasm })` with a URL, a `Response`, the bytes or a compiled module before `Minip2p.create`.

## Give the browser a node to dial

A native node serves browsers on a `/webtransport` listen address. With `@minip2p/node`:

```ts
import { Minip2p, generateSecretKey } from "@minip2p/node";

const server = Minip2p.create({
  secretKey: generateSecretKey(),
  listen: ["/ip4/127.0.0.1/udp/4001/quic-v1/webtransport"],
});

console.log(server.listenAddrs()[0]);
```

It prints an address carrying the hashes of the node's self-signed certificates, which the browser trusts instead of a certificate authority:

```text
/ip4/127.0.0.1/udp/4001/quic-v1/webtransport/certhash/uEi…/certhash/uEi…/p2p/12D3KooW…
```

The hashes derive from the node's key and a 13-day certificate period, so keep the key to keep the address across restarts. An address stays dialable for at least 13 days; `listenAddressesChanged` fires when the node rotates to the next period. A Rust node does the same with `listen_on("/ip4/0.0.0.0/udp/4001/quic-v1/webtransport")`, and the [relay server](/rust/host-a-relay) with `--webtransport`.

## Connect from the page

```ts
import { Minip2p, generateSecretKey } from "@minip2p/web";

const endpoint = Minip2p.create({ secretKey: generateSecretKey() });

const path = await endpoint.connect(address, { timeoutMs: 10_000 });
await endpoint.waitPeerReady(path.peerId, { timeoutMs: 10_000 });
console.log(`ping: ${await endpoint.ping(path.peerId)} ms`);
```

From here, [connections and streams](/typescript/connections-and-streams) work as on Node.js: register protocols with `protocols`, open streams, read with backpressure.

Serve the page from a secure context, HTTPS or `localhost`: WebTransport is unavailable otherwise. A page on a public origin that dials a private address may trigger Chrome's Local Network Access prompt; production nodes are dialed on public addresses.

## Relays

With `relays` (WebTransport addresses with certhashes) the browser holds a reservation on the relay and is reachable at its `/p2p-circuit` address (`node.circuitAddress`), from other browsers and from native nodes. Connecting by peer ID goes through the relay; connecting to a native peer's WebTransport address races the direct dial against the relay leg, so the path is `direct` or `relayed`, never `punched`: browsers never hole punch. `forceRelay: true` skips the direct dial. `activeReservation()` reports the reservation.

## Browser limits

`Minip2p.create` throws `InvalidConfigError`, with the reason, for what a browser cannot do:

| Option           | Why                                           |
| ---------------- | --------------------------------------------- |
| `listen`         | Browsers cannot listen.                       |
| `mdns`           | Browsers have no multicast sockets.           |
| `discovery`      | Signed discovery is not in the browser build. |
| `autonatServers` | Browsers cannot be dialed back.               |

- Only `/webtransport/certhash` addresses dial. A `/quic-v1` or `/tcp` candidate fails as a candidate, and there is no CA-certificate mode yet.
- `listenAddrs()` is empty (a reservation is `circuitAddress`); `knownPeers()` is empty and `reachability()` stays `unknown`.
- `close()` closes every session at once with WebTransport's close capsule, so its timeout bounds nothing.
- If the wasm module traps (a Rust panic), the endpoint reports `driverFailed` and later calls throw `ClosedError`. Create a new endpoint to carry on.

The [browser example](https://github.com/deepso7/minip2p/tree/main/examples/web) runs a Vite page against `minip2p-peer`.
