SDKs and platforms

How do I add mesh messaging to a React Native app?

Install @offline-protocol/mesh-sdk from npm, run pod install for iOS, add the Bluetooth usage keys and the Android runtime permission requests, then create an OfflineProtocol instance and call start(). The package is a native module with TypeScript types that wires Bluetooth LE, phone Wi-Fi peer streams, an internet relay and storage on iOS and Android, and encrypts messages with MLS by default. It runs in a native iOS or Android build of your app.

TL;DR

Install @offline-protocol/mesh-sdk, run pod install, declare the Bluetooth keys and request Android permissions before start(). The package runs both Bluetooth LE roles, relays messages across phones and can add Wi-Fi peer streams, a relay and Nostr. MLS encryption is on by default and fails closed. It needs a native build, so not Expo Go or a browser.

Learning objectives

After reading this article you will be able to:

  • Install @offline-protocol/mesh-sdk and declare the iOS and Android permissions it needs
  • Choose which transports to enable, from Bluetooth LE to Wi-Fi peer streams, the relay and Nostr
  • Explain what default MLS encryption does and where the package does not run

React Native mesh SDK requirements

The React Native package is @offline-protocol/mesh-sdk on npm, and as of 0.31.0 (October 2026) it is the stable, most complete Offline Protocol integration. It ships prebuilt native libraries for iOS and Android and TypeScript types, and it wires the transports and storage to the protocol engine for you. The other bindings, including the Swift and Kotlin previews, leave more of that wiring to the app; Which Offline Protocol SDK should I use? compares them.

The installation docs list these minimums:

RequirementMinimum
React Native0.70.0
React16.8.0
iOS deployment target13.0
Android minSdkVersion24
Android compileSdk and targetSdk34
JDK for the Android build17
Swift compiler5.10 (Xcode 15.3)

The SDK is dual-licensed: AGPL-3.0-only, or a commercial licence from Offline Protocol for apps that cannot meet the AGPL’s terms.

Installing @offline-protocol/mesh-sdk

Installing the package is one npm command, plus pod install for iOS:

npm install @offline-protocol/mesh-sdk
cd ios && pod install

Autolinking handles native linking on both platforms, so there is no pod line, post_install hook, settings.gradle include or MainApplication registration to add. The iOS binary ships as an XCFramework, and the docs state that device and simulator builds both work with no linker configuration. pod install should list MeshSdk under the autolinked modules. If you upgrade from a version below 0.20.0, delete any manual pod 'MeshSdk' line and any post_install hook you added for it, because they now break the build.

iOS and Android permissions for mesh messaging

Bluetooth access on iOS comes from Info.plist. Add NSBluetoothAlwaysUsageDescription and NSBluetoothPeripheralUsageDescription, and declare both bluetooth-central and bluetooth-peripheral under UIBackgroundModes. Without those background modes iOS can throttle scanning and advertising, and CoreBluetooth state restoration does not work. iOS shows its prompt on first Bluetooth use, so no runtime call is needed.

On Android, the SDK’s own manifest merges the Bluetooth, Wi-Fi, internet and foreground-service permissions into your app. Your manifest adds two it leaves to you: ACCESS_FINE_LOCATION (capped at API 30) for Bluetooth scanning on Android 11 and lower, and POST_NOTIFICATIONS. The second is easy to miss. start() launches a foreground service that posts a “Mesh Active” notification with a Stop action, and on Android 13 and later that notification is silently suppressed without the grant, leaving users no way to see or stop the mesh.

The SDK checks Android permissions but never requests them. Request BLUETOOTH_SCAN, BLUETOOTH_CONNECT and BLUETOOTH_ADVERTISE on Android 12 and later (ACCESS_FINE_LOCATION on 11 and lower), plus POST_NOTIFICATIONS on 13 and later, before calling start(). Bluetooth permissions on Android, explained covers why each one exists.

Transports the React Native SDK wires

The React Native SDK ships native bridges for five transport slots, and you enable each one in the transports section of the configuration:

TransportDefaultWhat it uses
bleOnBluetooth LE, scanning and advertising on every phone
wifiDirectOffAndroid: a Wi-Fi Direct group, and the Wi-Fi network it is on. iOS: Network framework over the local network or AWDL
internetOffA WebSocket connection to a relay you configure
nostrOffNostr relays you list as wss:// URLs
reticulumOffAn external Reticulum daemon or gateway, which the package does not ship

Bluetooth LE is the path for a first build. Because the SDK advertises and scans on each phone, two phones find each other without a separate peripheral module, which is the gap react-native-ble-plx leaves. Messages to a phone out of range are carried by the devices in between, with an 8-hop limit by default.

iPhones and Android phones meet over the Wi-Fi slot on a shared Wi-Fi network that allows multicast. An iPhone does not join Wi-Fi Direct groups, so with no shared network the two meet over Bluetooth LE or the relay. On iOS the slot also needs NSBonjourServices with _offlineprotocol._tcp and an NSLocalNetworkUsageDescription in Info.plist.

The Dynamic Offline Relay Switch (DORS) chooses among the enabled transports, and forceTransport() overrides it. Bluetooth LE plus Nostr is the same combination an app like Bitchat uses; How to build an app like Bitchat walks through it.

Default end-to-end encryption in the SDK

Messages are end-to-end encrypted with MLS (RFC 9420) by default. start() initializes MLS with the platform’s secure storage, keys are exchanged when peers are discovered, and messages to a peer whose session is still being set up wait in a queue and go out encrypted once it is ready.

Encryption also fails closed by default. If a message cannot be encrypted, for example because MLS was never initialized, the send fails with a typed error rather than going out in plaintext. To run an open, unencrypted mesh you opt out explicitly with requireEncryption: false, and each plaintext send then emits a security_warning event with the PLAINTEXT_SEND reason code, once per peer.

First mesh message in TypeScript

A first message needs an instance, a call to start() and a send to the peer’s address. This is the quick start from the SDK README:

import { OfflineProtocol, MessagePriority } from '@offline-protocol/mesh-sdk';

const protocol = new OfflineProtocol({
  appId: 'my-app',
  profile: 'user123',
  // Encryption is enabled by default!
});

// start() also initializes MLS with the platform's secure storage,
// unless encryption.enabled is false
await protocol.start();

// Messages are automatically encrypted!
const messageId = await protocol.sendMessage({
  recipient: peerAddress,  // the peer's off1… address; a username reaches nobody
  content: 'Hello!',  // Automatically encrypted
  priority: MessagePriority.Medium,
});

Register event listeners such as message_received, message_delivered and message_failed before calling start(), and keep one protocol instance per app process. The appId is a mesh app identifier you choose; it is not the developer portal’s App ID. Local messaging between phones needs no account or API key. The quickstart runs this on two physical phones over Bluetooth LE.

Expo, browser and device testing

The package is a native module, so it does not run in Expo Go; use a custom dev client or a bare React Native project. It does not run in a browser either. A separate, experimental browser build exists and is covered in Can Offline Protocol run in a web browser?.

A few other integration points to plan for:

  • Simulators. A simulator build checks that the app integrates and links. It does not test radio delivery, so test messaging on two physical phones.
  • Android after the process is killed. Optional mesh wake restarts the protocol from a headless task. It needs a manifest opt-in and module-scope registration, and React Native 0.76.5 or later when the New Architecture is on.
  • The internet relay. The hosted relay at wss://relay.offlineprotocol.com is turned on per application in the developer portal, and each device authenticates with its signed-in user’s OfflineID session token.

Frequently asked questions

Does @offline-protocol/mesh-sdk work with Expo?

Not in Expo Go. The SDK is a native module, so it needs a custom dev client or a bare React Native project.

Do I need an account or API key to send messages between two phones?

No. Two phones running the same mesh app identifier exchange encrypted messages over Bluetooth LE with no developer-portal account, key or relay. The hosted internet relay is the part that needs portal setup.

Can two phones running the SDK find each other over Bluetooth without another library?

Yes. On start() the SDK begins Bluetooth LE advertising and scanning on each phone, so it runs both roles itself. You do not add a separate peripheral module.

Does the SDK ask the user for Bluetooth permission on Android?

No. It checks permissions but never requests them. Your app requests them before start(), or the SDK emits an error-level diagnostic event saying Bluetooth permissions are missing.

Can I turn off encryption?

Yes, deliberately. Setting requireEncryption to false allows plaintext sends, and each plaintext send then emits a security_warning event with the PLAINTEXT_SEND reason code, once per peer.

Can I test mesh messaging in the iOS simulator?

Simulator builds compile and link, which checks the integration. They do not test radio delivery, so use two physical phones for that.

Sources

Build it with Offline Protocol

The React Native installation page lists the requirements, the Info.plist keys, what the SDK's Android manifest already declares, and the runtime permission code to run before start().

Read the React Native installation guide