Skip to main content
The Python binding supports desktop and gateway processes. Its peer-stream transport uses TCP over an existing IP network; it does not create a Wi-Fi Direct network. Internet relay and BLE integrations have separate platform requirements.

Install from PyPI

The package is 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:
Do not combine generated bindings from one revision with a native library from another.

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 under state_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 and ProtocolManager 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_root holds this device’s identity. Losing it gives the device a new address. Keep mls_root and state_root apart; 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_root at a fresh directory; a state directory written by another identity is refused with ProtocolError.InvalidConfiguration.
  • start() raises instead of starting without an identity. A wrong key raises ProtocolError.InvalidConfiguration and changes no record. A directory another process holds raises ProtocolError.InvalidState.
  • Each store locks its directory while open. await pm.close() stops the manager and releases both directories, and leaving async with does the same. Use stop() and start() to restart in place.
On a host without a keystore the store key sits wherever the process reads it from. The release threat model records this as residual risk R18.

Start a local peer

Save this as local_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.
This example uses the OS keyring. On a host without one, add the file-store arguments above to the 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

Enable internet_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 the offline-protocol-service command. When one instance sends for several applications without the local API, stamp a per-send application id with send_message_rich:
The id changes the stamp on that message only, never the storage namespace or the MLS session. 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.