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 matchingwasm-bindgen command-line tool, and build the module and its JavaScript glue into bindings/web/pkg:
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:
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 frombindings/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.
Link devices
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: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
fatallistener that starts the node again or tells the person the node ended. - A way to move the two WebRTC codes, or relays for a
NostrSignallerroom, 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
admitdecision for a nearby room. - A click that calls
BluetoothCarrier.request(), because the browser opens its chooser only inside a user gesture.
Reference
- Web binding contract: the worker, storage, WebRTC, rooms, the Nostr transport, Web Bluetooth, and the tests CI runs.
- Two-device checklist, with its recorded runs.
- Threat model, R26 to R28, R30 and R31: the store key, connection codes, room codes, nearby rooms and the Bluetooth chooser.
offline-protocol-webon crates.io.

