Install from PyPI
offline-protocol-sdk and the import name is offline_protocol_sdk. It supports Python 3.10 to 3.13. The lan extra adds DNS-SD discovery for peer streams; leave it out if you configure peers by address.
PyPI has a wheel for each of these platforms, with the native library inside:
glibc 2.34 means Debian 12, Ubuntu 22.04, RHEL 9 or newer. There is no wheel for Intel macOS or for musl-based Linux such as Alpine, and no source distribution, so
pip reports no matching distribution on those hosts. Build from source there.
The package metadata names AGPL-3.0-only. The SDK is dual-licensed, and a commercial license is available; see licensing.
Build from source
On a platform without a wheel, install the Rust toolchain and the native build prerequisites for your OS, then build the native library and the bindings from the same revision:Choose the storage
The SDK keeps two things on disk: the MLS store, which holds this device’s identity and sessions, and the protocol state understate_root, which holds the outbox, pending queues and delivery state. Preserve both across restarts.
On a headless Linux host,
keyring falls back to a backend that cannot hold keys, and the SDK logs a warning naming it. Use the file stores there.
Built-in file stores
Pass a store key andProtocolManager uses the SDK’s file stores instead of the keyring. Every MLS record is sealed on disk under that key.
- The store key is 32 random bytes, generated once and kept. Hand it to the process the way your service manager hands it any secret, or pass
store_key=<bytes>. Never derive it from a password or host name. mls_rootholds this device’s identity. Losing it gives the device a new address. Keepmls_rootandstate_rootapart; the same directory, or one inside the other, is refused.- Moving an existing keyring deployment onto the file stores starts a new identity. Point
state_rootat a fresh directory; a state directory written by another identity is refused withProtocolError.InvalidConfiguration. start()raises instead of starting without an identity. A wrong key raisesProtocolError.InvalidConfigurationand changes no record. A directory another process holds raisesProtocolError.InvalidState.- Each store locks its directory while open.
await pm.close()stops the manager and releases both directories, and leavingasync withdoes the same. Usestop()andstart()to restart in place.
Start a local peer
Save this aslocal_peer.py and run it from the activated environment. Each host uses its own state directory and its own MLS store. Allow TCP port 7878 and DNS-SD on the local network, or configure an explicit peer endpoint instead of discovery.
ProtocolManager call.
Run it on both hosts. The event stream identifies discovered peers. Use the resulting address when calling pm.send_message(peer_address, content) in your application. Binding 0.0.0.0 listens on every interface; use a specific interface address when the host spans networks. To skip discovery, pass peers=["off1...@10.0.0.7:7878"] or a plain "host:port" to configure.
An iPhone running Mesh SDK 0.28.0 with its wifiDirect transport enabled finds a Python host on the same LAN over this transport. Android Wi-Fi Direct does not interoperate with it.
Add an internet relay
Enableinternet_enabled and configure pm.internet with the provisioned relay URL. Call pm.internet.set_auth_token(auth_token) before pm.internet.start(). Without an explicit token, the v0.28.0 manager falls back to the profile string, which is forgeable and is not a production authentication policy. See relay access. A relay connection is an additional path; it does not replace local transport or automatically upload business records into your backend.
ProtocolManager builds its InternetManager for you. Code that constructs an InternetManager directly must pass app_id= from v0.28.0; it is keyword-only with no default.
Serve several local applications
A single process can own the engine and serve several local applications over the local API. The package ships the reference server as theoffline-protocol-service command.
When one instance sends for several applications without the local API, stamp a per-send application id with send_message_rich:
message_received carries app_id on the receiving side. It is cleartext and unsigned, so route on it and never authorize on it. An invalid id raises ProtocolError.InvalidArgument before anything is queued.
Other transports
wifi_direct_enabled=True enables the existing-IP peer-stream slot. Python BLE uses bleak for central and bless for peripheral. bless is installed by default except on Windows; the SDK starts the peripheral when BLE is enabled. Qualify platform radio support and permissions.
With reticulum_enabled=True, pm.gateway is a client for the gateway-daemon contract. Call pm.gateway.configure(daemon_address="localhost:4242") and await pm.gateway.start() after pm.start(). The daemon is deployed separately; see Reticulum and Nostr.
See the Python package README and the upgrade guide.
