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:
| Requirement | Minimum |
|---|---|
| React Native | 0.70.0 |
| React | 16.8.0 |
| iOS deployment target | 13.0 |
Android minSdkVersion | 24 |
Android compileSdk and targetSdk | 34 |
| JDK for the Android build | 17 |
| Swift compiler | 5.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:
| Transport | Default | What it uses |
|---|---|---|
ble | On | Bluetooth LE, scanning and advertising on every phone |
wifiDirect | Off | Android: a Wi-Fi Direct group, and the Wi-Fi network it is on. iOS: Network framework over the local network or AWDL |
internet | Off | A WebSocket connection to a relay you configure |
nostr | Off | Nostr relays you list as wss:// URLs |
reticulum | Off | An 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.comis turned on per application in the developer portal, and each device authenticates with its signed-in user’s OfflineID session token.