SDKs and platforms

Can Offline Protocol run in a web browser?

Yes, experimentally. The SDK's engine compiles to WebAssembly and runs in a dedicated worker as a full node with durable storage. It links tabs of one site, devices on one network over WebRTC, and any device through Nostr relays the application names, and in Chromium it can link to a phone or a board over Web Bluetooth. As of 0.31.0 the JavaScript package is not published, so you build it from the SDK repository.

TL;DR

The browser binding runs the same Rust engine in a worker as a full node, with sealed storage, WebRTC on a local network and Nostr through the application's relays. In Chromium it also links to a phone or a board over Web Bluetooth. It is experimental, and you build it from the SDK repository.

Learning objectives

After reading this article you will be able to:

  • Describe how the browser binding runs the engine, stores its state and links to other devices
  • Tell which capabilities work in Chromium, Firefox and Safari, including Web Bluetooth
  • Build the binding from source and know what a page must provide

Offline Protocol browser binding

Offline Protocol runs in a web browser as a full mesh node through the SDK’s browser binding, experimental as of 0.31.0 (October 2026). The engine is the same Rust as every other binding, compiled to WebAssembly in the offline-protocol-web crate on crates.io, and the JavaScript in bindings/web runs it. You build the JavaScript from the SDK repository while its package is not yet published.

A browser is a full node. It mints its own identity, forms MLS sessions, creates and administers groups, and relays like any other node. The 0.31.0 release added the worker engine, durable nodes, WebRTC between devices, Nostr over the application’s relays, browsers on one network finding each other, group member removal, and Web Bluetooth links. Offline Protocol’s OfflineID packages for React are a separate product; mesh in the browser comes from this binding.

Engine worker and durable storage

The engine runs in a dedicated worker, because the only browser storage that is synchronous and can flush, an origin private file system (OPFS) sync access handle, exists only in dedicated workers. The engine writes key and ratchet state it depends on immediately, so an acknowledged write must already be on disk. A worker also keeps encryption work off the thread a page renders on.

Storage is a sealed append log per category in origin-private files:

  • Every record is sealed with ChaCha20-Poly1305 and bound to its category, file generation and position, so a moved record does not authenticate.
  • The 32-byte store key is sealed under a non-extractable AES-GCM key kept in IndexedDB. This protects the files at rest, and script on the same origin can drive the node.
  • A Web Lock allows one node per account per origin. A second tab gets its own profile, or the page asks for storage: "memory", which is a new device on every start.

The page needs a secure context, HTTPS or localhost, because OPFS, Web Locks and WebRTC exist only there. If the engine panics, the worker ends and the page restarts the node; a durable node comes back as the same device with every acknowledged write.

A browser node links to other devices four ways, all built on what a page is allowed to do:

LinkWhat it reachesWhat it needs
BroadcastChannelOther tabs of the same originNothing
WebRTC data channelA device on the same local networkTwo connection codes carried by QR code or paste, or a room code through Nostr relays
Nearby roomBrowsers behind the same network addressThe application’s Nostr relays, a STUN server or a known network address, and an admit decision
Nostr transportAny device by its address, including mobile ones, and usernamesThe application’s Nostr relays

WebRTC needs no server by default: each side’s code carries every candidate, so a connection forms on the local network or not at all. An application that must cross networks passes its own STUN and TURN servers. The SDK names no relay and no STUN server, so every relay a node uses is one the application chose. Nostr carries the setup and, on the Nostr transport, encrypted messages; the data on a WebRTC link runs over the local network.

The browser also has ephemeral sends, for state the next send replaces such as a game’s positions. An ephemeral message goes once over a live link, with no acknowledgement or retry. The native bindings do not expose it yet.

In Chromium, a page links to a phone or a board running the SDK as a Bluetooth LE peripheral, a feature added in 0.31.0. The engine carries the same Bluetooth LE transport and fragment codec every binding uses, so the page holds the GATT connection and moves bytes while the engine verifies identity and encrypts.

Web Bluetooth makes the page a GATT central, so the link starts from the page:

  1. The person picks the device in the browser’s chooser, from a click, or the page reconnects to a device the origin was granted before.
  2. The page reads the device’s identity, and the engine verifies that it proves the device’s address.
  3. The page subscribes to messages and writes a Hello, which tells the peripheral who is writing.
  4. The engine announces the link, and an MLS session forms over it.

The iOS, Android and Python peripherals serve the Hello, the iOS one from 0.31.0. The carrier writes fragments at the SDK’s 185-byte floor, the size every binding falls back to. CI drives the carrier through a stand-in for navigator.bluetooth whose devices are other engines playing the peripheral, and runs with Chrome on Android, macOS and Linux against an Android phone and a board follow a manual checklist.

Browser support: Chromium, Firefox and Safari

The binding lists Chromium, Firefox and Safari, and CI runs its test pages in Chromium, Firefox and WebKit through Playwright. WebKit there is Playwright’s build of Safari’s engine.

CapabilityChromiumFirefoxSafari (WebKit)
Engine in a worker, tabs, WebRTC, Nostr, nearby roomsYes, tested in CIYes, tested in CIYes, tested in CI on WebKit
Storage conformance suites on real OPFSRun in ChromeRun in FirefoxNot run yet
Web BluetoothYesNoNo

Web Bluetooth is a Chromium feature: Firefox and Safari do not have it, and neither does any browser on iOS. Those browsers link over WebRTC and Nostr. Runs between two physical devices, including a phone’s browser and a tab the phone suspends, follow a manual checklist alongside CI.

Browser node scope

A browser reaches nearby devices the ways a page is allowed to: WebRTC on the local network, nearby rooms set up through relays, and Web Bluetooth in Chromium. Pages have no Bluetooth scanning, DNS-SD browsing or Wi-Fi Direct, so nearby rooms give browsers their local discovery. As of 0.31.0, a few parts of the native SDK are not in the browser build yet:

  • Offline Protocol relay transport. Nostr reaches the same devices from a browser, by address or username.
  • A project TURN server. The carrier takes the application’s own STUN and TURN servers for links across networks.
  • Telemetry. The uploader needs a thread and a blocking client; a browser build will need a fetch path for it.
  • A published JavaScript package. Build from source, as below.

For a phone app with Bluetooth LE, Wi-Fi Direct and relays wired for you, React Native is the stable and most complete integration. The platforms overview compares every binding.

Building the browser binding from source

To build the module and its JavaScript glue from a checkout of the SDK, install the pinned wasm-bindgen command-line tool once and run the build script:

cargo install wasm-bindgen-cli --version 0.2.105 --locked   # once
bash bindings/web/scripts/build.sh                          # module + glue in bindings/web/pkg

A page then starts one worker per node through OfflineNode.start({ appId }), listens for the fatal event, and serves the engine module where the worker can fetch it. examples/web-two-tabs is the first page to open in two tabs, and examples/web-lobby is a two-player game between devices over WebRTC with durable nodes. The SDK is AGPL-3.0-only, with a commercial licence for proprietary use.

Frequently asked questions

Is there an npm package for the Offline Protocol browser binding?

Not yet. As of 0.31.0 (October 2026) the JavaScript in bindings/web is private on npm, so you build the module and its glue from the SDK repository.

How does a web page connect to a phone over Bluetooth?

In Chromium, through Web Bluetooth as a Bluetooth LE central. The person picks the phone in the browser's chooser from a click, the engine verifies its identity, and the page links to the SDK running as a peripheral on the phone.

Does Web Bluetooth work in Safari, Firefox or on iPhone?

No. Web Bluetooth is a Chromium feature; Firefox and Safari do not have it, and neither does any browser on iOS. Those browsers link over WebRTC and Nostr.

Do two browsers need a server to connect?

Not on one local network. WebRTC is set up from two codes people carry by QR code or paste, with no ICE server by default. Relays for a short room code, and STUN or TURN servers for crossing networks, are optional and supplied by the application.

Is a browser node a lightweight client of a phone?

No. A browser is a full node. It mints its own identity, forms MLS sessions, creates and administers groups, and relays like any other node.

Where does a browser node keep its keys?

In sealed append logs in the origin private file system, under a store key that is itself sealed by a non-extractable key in IndexedDB. That protects the files at rest, and script running on the same origin can drive the node.

Sources

Build it with Offline Protocol

The platforms page of the docs lists every binding with its install route and the hosts it runs on. The browser binding's own guide is its contract in the SDK repository.

Read supported platforms