Skip to main content
From v0.31.0 the SDK runs in the browser. The engine is compiled to WebAssembly and runs as a full node inside a dedicated Web Worker: it mints its own identity, forms MLS sessions, creates and administers groups, and relays like any other node. The page moves bytes between devices; the engine proves who is on the other end of every link and encrypts every message. The binding is experimental. The engine is the offline-protocol-web crate on crates.io (0.31.0). The JavaScript package that runs it, bindings/web in the SDK repository, is not on npm yet, so you build it from the repository.

What a browser node does

Browsers

CI runs every test page in Chromium, Firefox and WebKit, and the storage conformance suites on real OPFS files in Chrome and Firefox. The SDK’s two-device checklist has been run between Chrome on a laptop and Chrome on an Android phone over one Wi-Fi network, in both directions.

Build it from the repository

Check out the SDK at the release tag, install the matching wasm-bindgen command-line tool, and build the module and its JavaScript glue into bindings/web/pkg:
The build script refuses a wasm-bindgen version that differs from the one in Cargo.lock. To see two nodes talk, serve the repository root and open the two-tabs example in two tabs:
Each tab shows its own off1… address and, within a second or two, the other tab as a neighbour with an encrypted session. The lobby example is a two-player game between devices with durable nodes, WebRTC links, a nearby room, an MLS group and ephemeral game state ten times a second.

Quick start

Import the client from bindings/web/src/index.js and serve bindings/web/src and the built bindings/web/pkg together, as the examples do: the worker loads the module from ../pkg. Every engine event is dispatched as a DOM event named by its type, with the engine’s JSON as detail:
OfflineNode also has establishSecureSession, createInvite, parseInvite, resolveUsername, createGroup, inviteToGroup, removeFromGroup, groupInfo, sendGroupMessage, stats and stop. The full surface, with each option, is in bindings/web/src/index.d.ts. Over WebRTC with two codes. new WebRtcCarrier(node) makes links between devices. One side calls offer() and shows its code; the other passes it to answer() and shows its answer; the first passes that to accept(). Each code carries every candidate, so it can cross by paste, QR code or a room. Pass iceServers with your own STUN or TURN servers when devices cannot reach each other directly; the SDK names none. A connection event reports connected, disconnected and closed for each link. With a room code. new NostrSignaller(node, { relays }) moves the two codes through your relays. The host calls host(room, carrier) with a code from newRoom(), and each joiner calls join(room, carrier). Generated room codes carry eighty bits, and a hosted room redials dropped relays and stays reachable until it is closed. In a nearby room. new NearbyRoom(node, carrier, { relays, iceServers, label, admit }) derives a room from the application id and the network’s public address, which a STUN server you name reports. Each device announces itself, sees the others in peers, and calls connect(peer) to link to one. Your admit callback decides every offer, so ask the person before you answer true. Over Web Bluetooth. Start the node with ble: true, create one BluetoothCarrier, and call request() inside a click to open the browser’s chooser. The carrier verifies the device’s identity, writes its hello, and only then announces the peer. known() returns devices this origin was granted before, so a page can reconnect without the chooser.

Reach devices over Nostr

Start the node with your relays to carry the Nostr transport from the worker:
A message or establishSecureSession to a device with no session and no live link asks the relays for the key packages that device published, and the session forms from the first one that opens. establishSecureSession answers "pending" until then. With usernameDiscovery, the node publishes its profile as a username and resolveUsername returns every claim for a name in one username_resolved event. The relays are always the application’s: start refuses nostr without a list of ws:// or wss:// URLs.

What your page provides

  • A secure context, and the engine module where the worker can fetch it, or passed to OfflineNode.start({ wasm }) when the worker starts from a Blob URL.
  • A fatal listener that starts the node again or tells the person the node ended.
  • A way to move the two WebRTC codes, or relays for a NostrSignaller room, and the joining address shown to the people involved before anything is shared with it.
  • Your own STUN or TURN servers to cross networks, your own Nostr relays for the Nostr transport and rooms, and an admit decision for a nearby room.
  • A click that calls BluetoothCarrier.request(), because the browser opens its chooser only inside a user gesture.
Framing, identity, encryption and storage are the engine’s. A browser build has no telemetry.

Reference

The SDK is AGPL-3.0-only with a commercial license available; see licensing.