> ## 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.28.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.28.0 (React Native), offline-protocol-sdk 0.28.0 on PyPI (Python 3.10 to 3.13; wheels for macOS 14+ arm64, Linux x86_64 and aarch64 with glibc 2.34+, Windows x86_64; other hosts build from source), the offline-protocol crates 0.28.0 on crates.io, the OfflineProtocolSDK Swift package 0.28.0 (preview, iOS 13+ only) and Android bindings from the GitHub release zip (preview, not on Maven Central), @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 account or API key. The Mesh SDK appId is a mesh app identifier the developer chooses, not the portal App ID (app_...) that OfflineID and Proof of Location require.
> Service RPC is signed plaintext in v0.28.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, named offline-protocol, runs with npx -y @offline-protocol/cli@0.2.6 mcp serve and requires no login or key. Hosted MCP is at https://mcp.offlineprotocol.com/mcp with an app API key in Authorization: Bearer and the matching App ID in x-app-id; organization keys are rejected, and Claude Desktop and Claude.ai cannot send these headers; there, use the public read-only endpoint https://mcp.offlineprotocol.com/public/mcp as a custom connector, or local MCP to scaffold or edit projects. 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.
> In v0.28.0 phone Wi-Fi peer streams carry data: Android over Wi-Fi Direct (the SDK forms the group with autoAccept on Android 10+), iOS over Network framework (LAN or AWDL). Android and iOS do not interoperate over that slot, and iOS 0.28 does not see iOS 0.27 or earlier over it; use BLE or a provisioned relay across platforms. The Swift and Android packages are previews whose public names may change; never present a Gradle Maven Central dependency for the Android library. 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.

# Local API service

> Run one Offline Protocol engine as a local service in v0.28.0 and let several applications on the host share its identity over JSON-RPC 2.0 on a WebSocket.

The SDK is usually embedded: one application, one process, one engine. The local API is the other shape. One process owns the engine and serves any number of local applications over a socket, so a host where several programs need the same identity, peers and documents runs one node instead of several. Each application speaks JSON-RPC 2.0 over a WebSocket, declares which application it is, and calls the engine's methods and receives its events by name.

The Python package ships the reference server as the `offline-protocol-service` command (module `offline_protocol_sdk.local_api`), from v0.28.0. Install the package from PyPI first; see [Python and Linux gateways](/docs/mesh-sdk/python).

## Guarantees for every client

* **The server owns the engine.** It runs the loop, drains inbound messages, starts and stops. No client can call `process()`, `receive_message()` or `stop()`.
* **A connection speaks for one application.** The first request is `hello` with an `app_id`. Every message that connection sends is stamped with it, and an inbound message stamped with it reaches only connections that declared it.
* **The socket is the authorization boundary.** Whoever can open the Unix socket, or holds the launch token on TCP, is one of the operator's applications. The server does not tell one local process from another.
* **Events are the engine's JSON, unchanged.** A client that handles `message_received` from the embedded SDK handles the same object here.

## Start the service

The service runs over the [built-in file stores](/docs/mesh-sdk/python#built-in-file-stores), so a host with no secret service needs only two directories and a key:

```bash theme={null}
export OFFLINE_PROTOCOL_STORE_KEY="$(openssl rand -hex 32)"   # once; keep it
offline-protocol-service --config config.json \
    --mls-root /var/lib/field-handoff/keys --state-root /var/lib/field-handoff/state \
    --socket /run/field-handoff/api.sock \
    --listen 0.0.0.0:7878 --peer 10.0.0.2:7878
```

`config.json` holds the `ProtocolConfig` fields by name, with enum values spelled as in the interface definition:

```json theme={null}
{
  "app_id": "field-handoff",
  "profile": "gateway",
  "ble_enabled": false,
  "wifi_direct_enabled": true,
  "internet_enabled": false,
  "reticulum_enabled": false,
  "nostr_enabled": false,
  "prefer_online": false,
  "initial_ttl": 8,
  "encryption_enabled": true,
  "auto_key_exchange": true,
  "store_pending": true,
  "require_encryption": true,
  "max_pending_per_peer": 64,
  "max_pending_global": 4096,
  "pending_ttl_ms": 86400000,
  "overflow_policy": "DropOldest"
}
```

`--listen` and `--peer` drive the peer-stream transport when the configuration enables it (`--peer` takes `host:port` or `off1...@host:port`). `--gateway HOST:PORT` names the gateway daemon when the configuration enables `reticulum` (default `localhost:4242`). `--keyring` uses the platform keyring instead of the file stores. Run `offline-protocol-service --help` for the full list.

| Carrier | Flags | Credential |
| - | - | - |
| Unix domain socket (default) | `--socket PATH` | The socket file is mode `0600`. `hello` carries no token. |
| Loopback TCP | `--tcp PORT --token-file PATH` | A 32-byte token, new at every launch, written to the file with mode `0600`. `hello` carries it. |

The server never binds a non-loopback address. To reach the API across a network, put a reverse proxy with its own authentication in front of it. `GET /health` on either carrier returns the server name and version, the API version and the carrier. There is no other HTTP path: a single call is a connection that sends `hello`, one request, and closes.

## Talk to it

Every request is a JSON-RPC 2.0 object with an `id`. Every event is a notification whose `method` is `event` and whose `params` is the event object.

```json theme={null}
{"jsonrpc":"2.0","id":1,"method":"hello","params":{"app_id":"notes","client":"notes-desktop/2.4"}}
{"jsonrpc":"2.0","id":1,"result":{"api_version":1,"server":{"name":"offline-protocol-service","version":"0.28.0"},"state":"Running","local_address":"off1..."}}
```

This Python client declares an application, narrows its events, and edits a replicated document. It needs only the `websockets` package the SDK already depends on.

```python theme={null}
import asyncio, json
from websockets.asyncio.client import unix_connect

SOCKET = "/run/field-handoff/api.sock"

async def main():
    async with unix_connect(SOCKET, uri="ws://localhost/") as ws:
        async def call(request_id, method, params):
            await ws.send(json.dumps({"jsonrpc": "2.0", "id": request_id,
                                      "method": method, "params": params}))
            while True:
                reply = json.loads(await ws.recv())
                if reply.get("id") == request_id:
                    return reply
                print("event:", reply["params"]["type"])

        hello = await call(1, "hello", {"app_id": "notes"})
        print("node:", hello["result"]["local_address"])
        await call(2, "subscribe", {"types": ["message_delivered", "message_undeliverable"]})
        await call(3, "data.create_doc", {"space_id": "notes-1", "doc_id": "todo"})
        await call(4, "data.map_set", {
            "space_id": "notes-1", "doc_id": "todo", "collection": "fields", "key": "title",
            "value_json": json.dumps({"kind": "text", "value": "groceries"}),
        })
        print((await call(5, "data.doc_json", {"space_id": "notes-1", "doc_id": "todo"}))["result"])

asyncio.run(main())
```

* **Sending.** Call the engine's methods by name with the parameters the interface definition gives them, for example `send_message` with `recipient`, `content` and `priority`. It returns the message ID. The `message_sent` and `message_delivered` events for that ID come back to the sending connection only. Watch for `message_undeliverable` as well.
* **Subscribing.** After `hello` a connection receives every event routed to its application. `subscribe` with `types` narrows that; `subscribe` with `"all"` resets it.
* **Held messages.** An inbound message for an application with no connected client is held, up to 256 per application ID, and delivered after the next `hello` under that ID. Delivery is at least once, so deduplicate on `message_id`.
* **Documents and services.** The replicated document store is reachable as `data.*` methods and mesh services as `services.*`. A write carries a tagged value; a read returns plain JSON. With the data layer off, every `data.*` call answers `DataDisabled`.
* **Errors.** A failed call is a JSON-RPC error whose `data.variant` is the engine's [`ProtocolError` variant](/docs/mesh-sdk/errors). Switch on the variant, not the number. A method before `hello` is `InvalidState`; a refused token, space or method is `PermissionDenied`.

Platform operations (driving a radio, attaching storage, feeding frames, running the loop, owning telemetry) are not on the wire, because the server owns them.

## Policy file

With no policy, any well-formed application ID is accepted. `--policy policy.json` adds rules per application ID:

```json theme={null}
{
  "spaces": {"notes": ["notes-*"], "mail": ["mail-*", "shared"]},
  "denied": {"kiosk": ["sign_data", "manual_mls", "tuning"]},
  "applications": ["admin"]
}
```

| Section | Effect |
| - | - |
| `spaces` | Glob patterns over document space IDs. A `data.*` call outside them is refused and document events for other spaces are not delivered. |
| `denied` | Method groups (`sign_data`, `manual_mls`, `tuning`) or single method names an application may not call. Refused with `PermissionDenied`. |
| `applications` | IDs with no rule of their own that are still admitted. |

Once `spaces` or `denied` names any application, an ID that no section names is refused at `hello`.

## Application IDs on the wire

The ID a client declares is stamped on each message it sends, in place of the configured `app_id`, and `message_received` and `file_received` report it on the receiving node. It changes only the stamp, never the storage namespace or the MLS session. The ID is cleartext and unsigned on the wire: route on it, never authorize on it. The same per-send ID is available without the service through `SendMessageOptions` and `MediaSendOptions` in Python and Rust, and the `appId` option of `sendMessage` and `sendMedia` in React Native.

## Reference

* [Local API guide](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/v0.28.0/docs/local-api.md) and [specification chapter](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/v0.28.0/docs/spec/local-api.md), with every method, event and error.
* [Python client example](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/v0.28.0/bindings/python/examples/local_api_client.py) over the Unix socket or TCP.
* [Node client example](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/v0.28.0/examples/local-api/client.mjs) for Node 22 or later with no dependencies, over TCP with the token file.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.