This guide walks you through integrating the Offline Protocol SDK into a new or existing React Native application. For internal development, this shows how to use the local SDK binding from the repository.
- Prerequisites
- Installation
- iOS Configuration
- Android Configuration
- Basic Integration
- Advanced Features
- Best Practices
- Common Pitfalls
- React Native 0.70.0 or higher
- Node.js 20 or higher
- iOS 12.0+ or Android API 21+
- Physical devices for BLE and Wi-Fi Direct testing
For internal development or testing unreleased SDK changes:
-
Add to package.json:
{ "dependencies": { "@offline-protocol/mesh-sdk": "file:../../bindings/react-native" } } -
Install dependencies:
npm install
For production apps:
npm install @offline-protocol/mesh-sdkcd ios
LANG=en_US.UTF-8 pod install
cd ..The use_native_modules! in your Podfile links the SDK automatically — no manual
pod line is needed. Both device and simulator builds are supported; CocoaPods
picks the matching slice out of the SDK's XCFramework.
Upgrading from a version before 0.20.0? Delete any manual
pod 'MeshSdk', ...line from your Podfile — autolinking now provides it, and a stale line will failpod install. Seedocs/UPGRADING.md.
Add required permissions to ios/YourApp/Info.plist:
<!-- Bluetooth permissions -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to communicate with nearby devices when offline</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app uses Bluetooth to communicate with nearby devices when offline</string>
<!-- Location permission (required for BLE scanning) -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to discover nearby devices for offline messaging</string>
<!-- Local network permission -->
<key>NSLocalNetworkUsageDescription</key>
<string>This app uses local network to discover and communicate with nearby devices</string>
<!-- Bonjour services -->
<key>NSBonjourServices</key>
<array>
<string>_offlineprotocol._tcp</string>
</array>
<!-- Background modes (REQUIRED for reliable BLE operation) -->
<!-- Without these, iOS will throttle/stop BLE causing false "peer lost" events -->
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>Ensure your ios/Podfile includes:
platform :ios, min_ios_version_supported
prepare_react_native_project!
target 'YourApp' do
config = use_native_modules!
use_react_native!(
:path => config[:reactNativePath],
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
# ... rest of configuration
endA stock React Native Podfile is all that's required — the SDK needs no dedicated
pod entry, no :modular_headers, and no post_install hook of its own.
npm run iosAdd required permissions to android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Internet permission -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Bluetooth permissions -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- Location permissions (required for BLE scanning) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Wi-Fi Direct permissions -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<!-- Features -->
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<uses-feature android:name="android.hardware.wifi.direct" android:required="false" />
<application ...>
<!-- Your app configuration -->
</application>
</manifest>Ensure your android/app/build.gradle includes:
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
react {
autolinkLibrariesWithApp()
}
android {
// ... your configuration
}
dependencies {
implementation("com.facebook.react:react-android")
// ... other dependencies
}The autolinkLibrariesWithApp() call automatically links the SDK.
npm run androidimport {
OfflineProtocol,
MessagePriority,
type ProtocolConfig,
type ProtocolEvent,
} from '@offline-protocol/mesh-sdk';Create src/hooks/useOfflineProtocol.ts:
import { useEffect, useState, useCallback, useRef } from 'react';
import {
OfflineProtocol,
ProtocolConfig,
ProtocolEvent,
MessagePriority,
} from '@offline-protocol/mesh-sdk';
export function useOfflineProtocol(config: ProtocolConfig) {
const [protocol, setProtocol] = useState<OfflineProtocol | null>(null);
const [isStarted, setIsStarted] = useState(false);
const [error, setError] = useState<string | null>(null);
const [events, setEvents] = useState<ProtocolEvent[]>([]);
const protocolRef = useRef<OfflineProtocol | null>(null);
// Initialize protocol
useEffect(() => {
try {
const instance = new OfflineProtocol(config);
protocolRef.current = instance;
setProtocol(instance);
// Listen to all events
instance.on('all', (event) => {
setEvents((prev) => [event, ...prev].slice(0, 100));
});
return () => {
instance.destroy().catch(console.error);
};
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to initialize');
}
}, [config.appId, config.userId]);
const start = useCallback(async () => {
if (!protocolRef.current) return;
try {
await protocolRef.current.start();
setIsStarted(true);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to start');
}
}, []);
const stop = useCallback(async () => {
if (!protocolRef.current) return;
try {
await protocolRef.current.stop();
setIsStarted(false);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to stop');
}
}, []);
const sendMessage = useCallback(
async (recipient: string, content: string, priority: MessagePriority) => {
if (!protocolRef.current || !isStarted) return null;
try {
return await protocolRef.current.sendMessage({
recipient,
content,
priority,
});
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to send');
return null;
}
},
[isStarted]
);
return { protocol, isStarted, error, events, start, stop, sendMessage };
}import React from 'react';
import { View, Text, Button } from 'react-native';
import { MessagePriority } from '@offline-protocol/mesh-sdk';
import { useOfflineProtocol } from './hooks/useOfflineProtocol';
export default function App() {
const { isStarted, start, stop, sendMessage } = useOfflineProtocol({
appId: 'my-app',
profile: 'user123',
transports: {
ble: { enabled: true },
internet: { enabled: true },
},
});
const handleSend = async () => {
const messageId = await sendMessage(
'user456',
'Hello!',
MessagePriority.Medium
);
console.log('Message sent:', messageId);
};
return (
<View>
<Text>Status: {isStarted ? 'Started' : 'Stopped'}</Text>
<Button
title={isStarted ? 'Stop' : 'Start'}
onPress={isStarted ? stop : start}
/>
<Button
title="Send Message"
onPress={handleSend}
disabled={!isStarted}
/>
</View>
);
}Handle specific events:
useEffect(() => {
if (!protocol) return;
// Handle message received
protocol.on('message_received', (event) => {
console.log(`From ${event.sender}: ${event.content}`);
// Update UI, show notification, etc.
});
// Handle transport switching
protocol.on('transport_switched', (event) => {
console.log(`Transport: ${event.from} → ${event.to}`);
// Update connection indicator
});
// Handle relay promotion
protocol.on('relay_promoted', (event) => {
console.log('Device is now a relay');
// Show relay status
});
return () => {
protocol.removeAllListeners();
};
}, [protocol]);const config: ProtocolConfig = {
// Required
appId: 'my-app-id',
profile: 'current-user-id',
// Transport configuration
transports: {
ble: { enabled: true }, // Enable Bluetooth Low Energy
wifiDirect: { enabled: true }, // Enable Wi-Fi Direct (Android)
internet: { enabled: true }, // Enable Internet connectivity
},
// DORS (Dynamic Offline Routing Strategy) configuration
dors: {
preferOnline: true, // Prefer online routes when available
switchHysteresis: 15, // Minimum score delta before switching transports
switchCooldownSecs: 20, // Cooldown between switches
bleToWifiRetryThreshold: 2, // Retries before escalating to Wi-Fi Direct
rssiSwitchThreshold: -85, // RSSI threshold for escalation
congestionQueueThreshold: 50, // Queue depth considered congested
stabilityWindowSecs: 8, // Sliding window for stability checks
poorSignalDurationSecs: 10, // Seconds RSSI must remain poor
ttlEscalationThreshold: 2, // TTL considered near exhaustion
congestionDurationSecs: 10, // How long congestion must persist
ttlEscalationHoldSecs: 20, // How long to keep TTL alarm sticky
historyWindowSize: 10, // Number of samples for DORS smoothing
queueRecoveryRatio: 0.5, // Ratio at which congestion is considered resolved
},
// Relay configuration
relay: {
allowRelay: true, // Allow device to act as relay
minBatteryForRelay: 20, // Minimum battery % to be relay
},
// Network configuration
network: {
initialTtl: 10, // Initial time-to-live for messages
},
};import { PermissionsAndroid, Platform } from 'react-native';
async function requestPermissions() {
if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
]);
return Object.values(granted).every(
(status) => status === PermissionsAndroid.RESULTS.GRANTED
);
}
return true;
}
// Use before starting protocol
const hasPermissions = await requestPermissions();
if (hasPermissions) {
await protocol.start();
}Always handle errors:
try {
await protocol.start();
} catch (error) {
console.error('Failed to start protocol:', error);
// Show user-friendly error message
Alert.alert('Error', 'Failed to start offline protocol');
}Properly clean up:
useEffect(() => {
return () => {
protocol?.destroy();
};
}, [protocol]);Remove listeners when not needed:
useEffect(() => {
const handler = (event) => {
console.log('Message:', event);
};
protocol?.on('message_received', handler);
return () => {
protocol?.off('message_received', handler);
};
}, [protocol]);Use a custom hook or state management library:
// Option 1: Custom hook (recommended for simple apps)
const { isStarted, events } = useOfflineProtocol(config);
// Option 2: Redux/Zustand (for complex apps)
// Store protocol state in global stateUse TypeScript for type safety:
import type {
MessageReceivedEvent,
TransportSwitchedEvent,
} from '@offline-protocol/mesh-sdk';
protocol.on('message_received', (event: MessageReceivedEvent) => {
// event is fully typed
console.log(event.sender, event.content);
});Problem:
await protocol.start(); // May fail without permissionsSolution:
await requestPermissions();
await protocol.start();Problem:
const protocol1 = new OfflineProtocol(config);
const protocol2 = new OfflineProtocol(config); // Don't do this!Solution:
// Use a single instance throughout the app
// Manage it with a hook or contextProblem:
useEffect(() => {
const p = new OfflineProtocol(config);
p.start();
// No cleanup!
}, []);Solution:
useEffect(() => {
const p = new OfflineProtocol(config);
p.start();
return () => {
p.destroy();
};
}, []);Problem:
const protocol = new OfflineProtocol(config);
await protocol.sendMessage({ ... }); // Protocol not started!Solution:
const protocol = new OfflineProtocol(config);
await protocol.start();
await protocol.sendMessage({ ... });Problem:
// Not listening to events means missing important updatesSolution:
protocol.on('all', (event) => {
console.log('Event:', event.type);
// Handle events appropriately
});Problem:
const config = {
appId: 'my-app',
profile: 'user123',
};
// ...then treating 'user123' as this device's address:
sendTo(peerId, { from: 'user123' }); // wrong — no peer knows this stringprofile selects which stored identity this instance runs as. It never leaves
the device. Your identity on the wire is the off1… address the SDK derives
from a key it generates for itself.
Solution:
const config = {
appId: 'my-app',
profile: getCurrentUserId(), // one namespace per account on this device
};
// Read the real identity once it exists:
const myAddress = await protocol.localAddress(); // "off1q…" | null
protocol.on('identity_ready', ({ address }) => setMyAddress(address));Use profile to keep accounts separate on a shared device, and the address for
anything a peer sees: recipient, "is this message mine", conversation keys.
Before deploying:
- Tested on physical iOS device
- Tested on physical Android device
- Verified all permissions are requested
- Tested with internet enabled
- Tested with internet disabled (BLE only)
- Tested message sending between devices
- Verified event handling works
- Tested protocol start/stop lifecycle
- Checked for memory leaks
- Reviewed error handling
- Explore the example app to see a complete implementation
- Review the API reference for detailed method documentation
- Check the architecture docs to understand SDK internals
- Join the community for support and discussions
For issues or questions:
- Check this integration guide
- Review the example app
- Read the API reference
- Open an issue on GitHub