> ## Documentation Index
> Fetch the complete documentation index at: https://www.offlineprotocol.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These docs target Mesh SDK v0.27.0. Match the installed package and binding before generating code. Start at /getting-started/agents for task-specific reading paths.
> Call the company and product Offline Protocol, never Offline alone. Current packages: @offline-protocol/mesh-sdk 0.27.0 (React Native), @offline-protocol/id-react 0.2.0, @offline-protocol/id-react-native 0.3.3, @offline-protocol/pol 0.1.2 and @offline-protocol/cli 0.2.6. Canonical docs URLs start with https://www.offlineprotocol.com/docs.
> The Mesh SDK runs in a native app or gateway. A browser OfflineID SDK integration does not provide browser mesh transport. Local mesh operation does not require a portal API key.
> Service RPC is signed plaintext in v0.27.0. Message delivery, durable local acceptance and backend commit are distinct outcomes. Use the workflow guide for the required application logic.
> Offline Protocol CLI 0.2.6 is on npm (@offline-protocol/cli, command offline). Its local MCP server runs with offline mcp serve and requires no login or key. Hosted MCP is at https://mcp.offlineprotocol.com/mcp with an application API key in Authorization: Bearer and a matching x-app-id; organization keys are rejected. Follow /tools/overview for setup and do not invent commands beyond it. MCP provides integration context and planning, not mesh execution; file-writing tools are local only.
> Phone Wi-Fi Direct and MultipeerConnectivity carry no data in v0.27.0. Use BLE or a provisioned relay. The receiver core ACKs before application persistence; use application acceptance for durable workflows.
> Proof of Location is Sepolia testnet witness evidence, not zero-knowledge proof or proof of presence. The geohash is public onchain. Read /proof-of-location/security before integration.

# Run a two-phone BLE app

> Build a bare React Native host app with the published Mesh SDK v0.27.0 package and exchange messages between two phones over Bluetooth LE.

Use two physical phones, Bluetooth enabled, with the app in the foreground. This walkthrough pins React Native 0.76.5 and Mesh SDK 0.27.0. It uses the native libraries in the npm package; a Rust checkout, NDK build of the SDK and local `file:` dependency are not required.

This walkthrough is typechecked against the pinned packages. For complete apps on the same package, see [offline-app-examples](https://github.com/Offline-Protocol/offline-app-examples): four React Native apps on Mesh SDK 0.27.0.

## Create the host

Install the platform development tools from the [React Native environment guide](https://reactnative.dev/docs/set-up-your-environment): Node, JDK 17 and Android Studio for Android; Xcode and CocoaPods for iOS. Use Node 22 for this walkthrough.

```bash theme={null}
npx @react-native-community/cli@15.0.1 init OfflineMeshDemo --version 0.76.5
cd OfflineMeshDemo
npm install @offline-protocol/mesh-sdk@0.27.0
```

For iOS, install pods:

```bash theme={null}
cd ios
bundle install
bundle exec pod install
cd ..
```

Apply the [Info.plist and Android manifest entries](/docs/mesh-sdk/installation-rn). BLE permissions are required; phone Wi-Fi Direct and MultipeerConnectivity cannot carry traffic in SDK 0.27.0. Do not enable Android mesh wake for this foreground exercise.

## Add the messaging module

Create `mesh.ts` in the project root. Copy the complete TypeScript module from [Start and discover a peer](/docs/getting-started/quickstart#2-start-and-discover-a-peer), including its imports and exported functions.

Create `permissions.ts` in the project root. Copy the complete `requestMeshPermissions` helper, including its React Native imports, from [Android runtime permissions](/docs/mesh-sdk/installation-rn#android). The helper returns immediately on iOS; the system prompts when Bluetooth starts.

## Add the screen

Replace `App.tsx` with:

```tsx theme={null}
import React, { useEffect, useState } from 'react';
import { Button, SafeAreaView, ScrollView, Text, View } from 'react-native';
import type { ProtocolEvent } from '@offline-protocol/mesh-sdk';
import { protocol, discoveredPeers, startMesh, stopMesh, sendUpdate } from './mesh';
import { requestMeshPermissions } from './permissions';

export default function App() {
  const [running, setRunning] = useState(false);
  const [busy, setBusy] = useState(false);
  const [peers, setPeers] = useState<string[]>([]);
  const [lines, setLines] = useState<string[]>([]);
  const log = (text: string) => setLines(previous => [text, ...previous].slice(0, 30));

  useEffect(() => {
    const receive = (event: ProtocolEvent) => {
      if (event.type === 'message_received') log(`Received from ${event.sender}: ${event.content}`);
      if (event.type === 'message_delivered') log(`SDK delivery: ${event.message_id}`);
      if (event.type === 'message_failed') log(`Failed: ${event.reason}`);
      if (event.type === 'neighbor_discovered') log(`Discovered over ${event.transport}: ${event.peer_id}`);
    };
    protocol.on('all', receive);
    const timer = setInterval(() => {
      setPeers([...discoveredPeers]);
    }, 500);
    return () => {
      clearInterval(timer);
      protocol.off('all', receive);
    };
  }, []);

  async function run(action: () => Promise<void>) {
    setBusy(true);
    try { await action(); }
    catch (error) { log(String(error)); }
    finally { setBusy(false); }
  }

  return (
    <SafeAreaView style={{ flex: 1 }}>
      <ScrollView contentContainerStyle={{ padding: 20, gap: 16 }}>
        <Text style={{ fontSize: 24 }}>Mesh Demo</Text>
        <Text>{running ? 'Running over BLE' : 'Stopped'}</Text>
        <Button title={running ? 'Stop mesh' : 'Start mesh'} disabled={busy} onPress={() => void run(async () => {
          if (running) { await stopMesh(); setRunning(false); }
          else {
            if (!(await requestMeshPermissions())) throw new Error('Bluetooth permissions required');
            await startMesh(); setRunning(true);
            log(`Local address: ${await protocol.localAddress()}`);
          }
        })} />
        {peers.map(peer => (
          <View key={peer} style={{ gap: 8 }}>
            <Text selectable>{peer}</Text>
            <Button title="Send inspection update"
              disabled={!running || busy}
              onPress={() => void run(async () => { log(`Queued: ${await sendUpdate(peer)}`); })} />
          </View>
        ))}
        <Text>Received content is displayed only. This demo does not store a durable handoff.</Text>
        {lines.map((line, index) => <Text selectable key={index}>{line}</Text>)}
      </ScrollView>
    </SafeAreaView>
  );
}
```

Fast Refresh must not create a second native protocol instance. After editing the module that constructs `OfflineProtocol`, perform a full app reload. Do not call `destroy()` when stopping and restarting this screen.

## Run and check

Start Metro with `npm start`. In another terminal run `npm run android` or `npm run ios`, choosing a physical device. Install and launch the same app on the second phone.

1. Tap **Start mesh** on both phones and grant permissions.
2. Check the discovered `off1` address and reported `ble` transport.
3. Tap **Send inspection update** for the discovered peer. The SDK queues the message until its encrypted session is ready; a queued ID is not delivery.
4. Confirm the JSON appears on the receiving phone and the sender reports SDK delivery.
5. Turn off cellular and Wi-Fi, leaving Bluetooth enabled. Repeat.
6. Stop and start both instances and repeat to check listener reuse.

This is a foreground discovery and messaging exercise. Add [durable application storage and acceptance](/docs/guides/local-handoff) before using it for an operational handoff. If no peer appears, check [troubleshooting](/docs/operations/troubleshooting).
