Offline Protocol on servers
Offline Protocol runs on Linux servers through the bindings built for desktop and server processes. As of 0.31.0 (October 2026):
| Binding | On a server | Transports |
|---|---|---|
Python, offline-protocol-sdk (beta) | Wheels for Linux x86_64 and aarch64 with glibc 2.34 or later | Ships its own: internet relay, LAN peer streams, Bluetooth LE, gateway client |
Python service, offline-protocol-service | Installed with the package; also a container image | Starts every transport its configuration enables |
Java, offline-protocol-java | JDK 22 or later, over the release’s Linux library | Your process supplies them |
| C and languages that call C | offline_protocol.h and the release’s Linux library | Your process supplies them |
Ruby, offline_protocol | RubyGems, over the release’s Linux library | Your process supplies them |
Rust, offline-protocol | crates.io | You write the platform bridge |
Python is the shortest path, because it is the only one of these that ships transport bridges. With the others, your code drains each transport’s outbound queue, does the I/O and reports the result.
The SDK is AGPL-3.0-only, including its network-use source-disclosure requirement, or available under a commercial licence for organisations that cannot meet it, such as a SaaS that does not release its source. The licensing docs have the terms.
Python SDK on a Linux server
The Python package runs a full peer on any Linux x86_64 or aarch64 server with glibc 2.34 or later, which the docs equate with Debian 12, Ubuntu 22.04, RHEL 9 or newer. There is no wheel for musl-based Linux such as Alpine or for an older glibc, and no source distribution, so those hosts build from source.
A host that runs the service wants both extras: lan finds other hosts on the LAN over DNS-SD, and http adds the HTTP front.
pip install 'offline-protocol-sdk[lan,http]'
How do I use Offline Protocol from Python? covers the API itself.
Store key for hosts without a keyring
A server or container usually has no secret service, so the keyring cannot hold the device’s keys. On a headless Linux host keyring falls back to a backend that cannot hold keys, and the SDK logs a warning. Pass a store key instead, and the SDK uses its built-in file stores, sealing every MLS record on disk under that key:
pm = ProtocolManager(
config,
store_key_env="OFFLINE_PROTOCOL_STORE_KEY", # 64 hex digits or base64
mls_root="/var/lib/example/keys", # or OFFLINE_PROTOCOL_MLS_ROOT
state_root="/var/lib/example/state", # or OFFLINE_PROTOCOL_STATE_ROOT
)
await pm.start()
The rules that matter in operation:
- The key is 32 random bytes, generated once and kept. Generate it with
openssl rand -hex 32and hand it over the way the service manager hands over any secret. Never derive it from a password or a host name. mls_rootis the device’s identity. Losing it, or the key, gives the device a new address. Keepmls_rootandstate_rootapart; the same directory, or one inside the other, is refused.- Moving a 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. start()raises rather than run without the identity. A wrong key raisesProtocolError.InvalidConfigurationand changes no record.
On a host without a keystore the store key sits wherever the process reads it from, which the SDK’s threat model records as residual risk R18. Java, C and Ruby reach the same stores through initialize_mls_with_file_stores, and Rust through FileStorePair::open with EnvStoreKey, which reads the same environment variable.
Local API service with offline-protocol-service
offline-protocol-service runs one engine and serves any number of local programs over JSON-RPC 2.0 on a WebSocket, so several programs on a host share one identity instead of each running a node. A client sends hello with its application id and then calls the engine’s methods by name.
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
The default carrier is a Unix socket with mode 0600; --tcp PORT --token-file PATH serves loopback TCP with a token that is new at every launch. The socket is the authorization boundary, and the server does not tell one local process from another. The local API never binds a non-loopback address, so reaching it across a network takes a reverse proxy with its own authentication. Other Offline Protocol hosts reach this one over the peer stream on port 7878, not through the API.
A transport that fails to start is logged and the others run. offline-protocol-verify drives the running service and waits for the events that show a message was held, carried and delivered, which makes it a deployment check.
HTTP front for programs without the SDK
With the http extra, --http 127.0.0.1:8080 starts the HTTP front in the same process. A program that does not import the SDK then calls a service on another device with an ordinary HTTP request to <service>.<device>.offline.protocol.internal, carried inside end-to-end encrypted messages. How can a program call an HTTP service on another device with no internet? explains the approach.
The front does not authorize callers. Any device whose message the engine decrypts can call every registered service, so the provider decides whom it serves, for example by checking X-Offline-Protocol-Sender against addresses it received out of band. Off loopback, the front requires a per-launch token. Bodies are limited to 128 KiB each way, and a 504 does not mean the request did not run.
Offline Protocol in Docker
The SDK repository’s bindings/python/docker directory builds one service per host on python:3.12-slim, with the lan and http extras installed from PyPI and the front on 127.0.0.1:8080. From that directory, on a Linux host:
export OFFLINE_PROTOCOL_STORE_KEY="$(openssl rand -hex 32)" # once per host; keep it
docker compose up -d
The container README at 0.31.0 also lists a published image, built from that directory for linux/amd64 and linux/arm64:
docker pull ghcr.io/offline-protocol/offline-protocol-service:0.31.0
Pin a deployment to the image digest rather than the tag, because a registry tag can be moved. Whatever runs the container owes it these grants:
| Grant | Why |
|---|---|
| The host’s network namespace | Multicast DNS does not leave a container bridge, and the front then sits on the host’s loopback. Set OP_LISTEN to the host’s reserved LAN address. |
| Inbound TCP 7878 | Other hosts open their peer streams here. |
A persistent volume at /var/lib/offline-protocol | The identity and every queued message. Without it the host has a new address after every recreate. |
OFFLINE_PROTOCOL_STORE_KEY from the host’s secret store | Everything on the volume is sealed under it. |
| The system D-Bus socket, for Bluetooth LE | The service advertises to phones through BlueZ. |
| A clock | A provider drops a request more than 60 seconds past its deadline. |
On Docker Desktop for macOS and Windows, host networking is the namespace of Docker’s Linux virtual machine, so the front is not on the desktop’s loopback and multicast DNS does not reach its LAN. Use the SDK’s demos there and a Linux host for deployment.
The README describes a run of the image: two containers on one bridge network found each other over multicast DNS with no peer list, and served a request through the HTTP front from one to a provider on the other, end to end encrypted, in 35 ms. The address and the front’s registrations survived a restart of the container.
Rust, Java and C on servers
The Rust crates, the Java binding and the C API run on servers as engines without transports. They suit a server process that already owns its sockets and wants the protocol engine in-process rather than over the local API.
- Rust.
cargo add offline-protocol@=0.31.0. The crates are I/O-free, so you write the platform bridge. The defaultfile-storefeature providesSealedFileMlsStorageandFileProtocolStateStorage, which write the same state format as the other bindings. See how to use Offline Protocol from Rust. - Java. JDK 22 or later, with the Linux x86_64 or aarch64 library from the GitHub release. See how to use Offline Protocol from Java.
- C, C++, Go and C#. The header and the shared library from the same archives. See how to call Offline Protocol from C, C++, Go or C#.
- Ruby. Ruby 3.3 or later and the
ffigem, over the same library; see how to use Offline Protocol from Ruby.