offline-protocol-sdk on PyPI
The Python binding is offline-protocol-sdk on PyPI, imported as offline_protocol_sdk. Its package metadata marks it beta, it requires Python 3.10 or later, and it lists Python 3.10 to 3.13. It runs the same Rust protocol engine as the phone SDKs, so a Python process is a full peer: it has its own address, encrypts with MLS (RFC 9420), relays for others and keeps delivery state across restarts.
The usual uses are a desktop tool, a gateway or a site computer that exchanges messages and shared state with phones. If you are still choosing a Python library for peer-to-peer networking in general, Which Python libraries do peer-to-peer and mesh networking? compares the options by layer. The package metadata names AGPL-3.0-only, and a commercial licence is available.
Python wheels and supported hosts
Python wheels on PyPI carry the native library, so on a supported host there is nothing to compile:
| Host | Wheel tag | Requires |
|---|---|---|
| macOS on Apple silicon (arm64) | macosx_14_0_arm64 | macOS 14 or later |
| Linux x86_64 | manylinux_2_34_x86_64 | glibc 2.34 or later |
| Linux aarch64 | manylinux_2_34_aarch64 | glibc 2.34 or later |
| Windows x86_64 | win_amd64 |
The docs translate glibc 2.34 as Debian 12, Ubuntu 22.04, RHEL 9 or newer. There is no wheel for Intel macOS, musl-based Linux such as Alpine, or an older glibc, and no source distribution, so pip reports no matching distribution there. On those hosts, clone the SDK at a release tag and build the native library and the bindings together with the Rust toolchain (1.91 or newer), and never mix bindings from one revision with a library from another.
lan and http extras and the installed commands
The base install is enough to run a peer from your own Python code:
pip install offline-protocol-sdk
A host that runs the service, such as a gateway or a site computer, usually also wants the two extras. lan adds DNS-SD discovery for peer streams through python-zeroconf, and http adds the HTTP front through aiohttp. Both are imported only when used.
pip install 'offline-protocol-sdk[lan,http]'
Every install puts three commands on the path:
| Command | What it does |
|---|---|
offline-protocol-service | Serves one engine to local applications over the local API; --lan needs the lan extra and --http the http extra |
offline-protocol-http-front | Runs the HTTP front on its own, against a service already running |
offline-protocol-verify | Drives the local service and waits for the events that show a message was held, carried and delivered |
Python transports and phone reach
Python transports are built by ProtocolManager from flags in ProtocolConfig, and each one has a different reach:
| Transport | Config flag | What it does in Python |
|---|---|---|
| Internet relay | internet_enabled | WebSocket client over websockets |
| Bluetooth LE | ble_enabled | Central role over Bleak on every platform; peripheral role over Bless on macOS and Linux, not Windows |
| Peer stream | wifi_direct_enabled | TCP streams over an existing IP network, to listed host:port peers or hosts found over DNS-SD |
| Gateway | reticulum_enabled | A client for a separately deployed gateway daemon, on local IP |
| Nostr | nostr_enabled | A stub callback; apps that drive Nostr themselves replace it |
The peer stream takes the wifi_direct slot’s name, but it does not create a Wi-Fi Direct network. It is how phones reach a Python host on a local network: an iPhone on 0.28.0 or later and an Android phone on 0.29.0 or later, with their Wi-Fi transport enabled, find the host on the same Wi-Fi network when it allows multicast. Over Bluetooth LE, the Python peripheral lets a phone start a session with the host, and the docs record it running between an Android phone and Macs and Raspberry Pis.
This is the SDK README’s minimal relay example:
import asyncio
from pathlib import Path
from offline_protocol_sdk import ProtocolManager
from offline_protocol_sdk.offline_protocol import ProtocolConfig, OverflowPolicy
config = ProtocolConfig(
app_id="my-app",
profile="alice",
ble_enabled=False,
wifi_direct_enabled=False,
internet_enabled=True,
reticulum_enabled=False,
nostr_enabled=False,
prefer_online=True,
initial_ttl=3,
encryption_enabled=True,
auto_key_exchange=True,
store_pending=True,
require_encryption=False,
max_pending_per_peer=100,
max_pending_global=1000,
pending_ttl_ms=86_400_000, # 24 h (the SDK default)
overflow_policy=OverflowPolicy.DROP_OLDEST,
)
# The peer's address: its pm.local_address. A username reaches nobody.
peer_address = "off1..."
async def main():
# The installer must remove this application-owned directory on uninstall.
state_root = Path("/app/install-owned-data/offline-protocol")
async with ProtocolManager(
config,
event_handler=print,
state_root=state_root,
) as pm:
pm.internet.configure(server_url="ws://relay.example.com")
await pm.internet.start()
msg_id = pm.send_message(peer_address, "Hello from Python!")
print(f"Sent: {msg_id}")
# Keep running to receive messages
await asyncio.sleep(30)
asyncio.run(main())
Python key storage on desktops and servers
Python key storage has two options. On a desktop with a secret service (macOS Keychain, GNOME Keyring or KWallet, Windows Credential Locker), the MLS identity goes into the OS keyring through keyring, and you pass a state_root for the protocol state.
A server, container or headless Linux host usually has no secret service, and there keyring falls back to a backend that cannot hold keys. Pass a store key instead, with store_key_env or store_key, plus separate mls_root and state_root directories, and ProtocolManager uses the SDK’s built-in file stores, sealing every MLS record under that key. The key is 32 random bytes generated once and kept. Losing mls_root gives the device a new address. Can I run Offline Protocol on a server or in Docker? goes further into headless hosts.
Local API service for several applications
The local API lets one process own the engine and serve several applications on the same host over JSON-RPC 2.0 on a WebSocket, so they share one identity, one set of peers and one document store. The offline-protocol-service command is the reference server:
export OFFLINE_PROTOCOL_STORE_KEY="$(openssl rand -hex 32)" # once; keep it
offline-protocol-service --config config.json \
--mls-root /var/lib/example/keys --state-root /var/lib/example/state \
--socket /run/example/api.sock --listen 0.0.0.0:7878 --peer 10.0.0.2:7878
config.json holds the ProtocolConfig fields by name. The service starts every transport the configuration enables: --lan finds other hosts with no peer list, ble_enabled starts both Bluetooth LE roles so phones can find the host, and --relay connects the internet transport. A client opens the Unix socket (owner-only by default), sends hello with its application ID, then calls the engine’s methods and receives its events by name. The local API never binds a non-loopback address, and whoever can open the socket counts as one of the operator’s applications.
HTTP front for programs without the SDK
The HTTP front lets a program that does not import the SDK call a service on another device with an ordinary HTTP request. Start the service with --http 127.0.0.1:8080, register a provider’s local endpoint with a PUT /services/<service>, and a requester on another host sends its request to its own front with a host name of the form <service>.<device address>.offline.protocol.internal. The fronts carry the request and response inside end-to-end encrypted messages, over whichever carrier reaches the other host.
Two numbers shape the design. Bodies are capped at 128 KiB in either direction, and the deadline defaults to 30 seconds. The front does not authorize callers, so the provider decides who it serves, for example by checking the X-Offline-Protocol-Sender header against an allowlist. How can a program call an HTTP service on another device with no internet? covers the pattern in more depth.