SDKs and platforms

How do I use Offline Protocol from Java?

Offline Protocol publishes a typed Java binding on Maven Central as com.offlineprotocol:offline-protocol-java for the JVM on desktop and server. It calls the SDK's native library through the Java foreign function API, so it needs JDK 22 or later and no JNA or JNI. The jar carries no native code: you download the library for your platform from the GitHub release and supply the transports and storage yourself.

TL;DR

The Java binding is a jar on Maven Central that calls the SDK's native library through the foreign function API, so it needs JDK 22 or later. You add the library for your platform from the GitHub release and write the transports and storage, or use the built-in file stores. A runnable two-device example shows the whole flow.

Learning objectives

After reading this article you will be able to:

  • Add the Java binding from Maven Central and load the matching native library on JDK 22 or later
  • List what a Java host must supply, from transports and storage to the telemetry platform
  • Run the two-device Java example that the SDK's CI runs

Offline Protocol Java binding

The Offline Protocol Java binding is a typed Java API for the JVM on desktop and server, added in version 0.30.0. It is generated by uniffi-bindgen-java from the same interface definition as the Swift, Kotlin, Python and Ruby bindings, so the engine, records and errors match the other languages.

It calls the native library directly through the Java foreign function API. There is no JNA, no JNI and no hand-written native glue, and it does not go through the C API. The package is com.offlineprotocol.sdk:

  • OfflineProtocol is the engine, and MeshServices and DataStore hang off it.
  • Every record and enum of the definition is a class.
  • What a host implements are interfaces: EventCallback, the transport callbacks and the two storage providers.
  • An engine refusal is a subclass of ProtocolException named for the variant, such as ProtocolException.EncryptFailed.
  • OfflineProtocolLib holds the functions that need no engine, such as deriveAddress, parseInvite and runStorageConformance.

It is not the Kotlin binding. On Android, use the Android library instead; this jar is for any other JVM.

Maven Central coordinates and JDK 22

The jar is on Maven Central as com.offlineprotocol:offline-protocol-java, published at each release’s version beside the Android library:

// build.gradle.kts
dependencies {
    implementation("com.offlineprotocol:offline-protocol-java:0.31.0")
}

The minimum is JDK 22, because the foreign function API is final in Java 22 and the code is compiled with --release 22. The JVM must also be allowed to call native code: pass --enable-native-access=ALL-UNNAMED, or your module’s name. Without it the JVM warns on the first native call, and a future JDK will refuse it.

Native library for each host

The jar holds the generated Java and no native code, so each host downloads the native library from the GitHub release’s desktop archives:

HostLibrary
Linux x86_64 and aarch64liboffline_protocol_uniffi.so
macOS arm64liboffline_protocol_uniffi.dylib
Windows x86_64offline_protocol_uniffi.dll

Use a jar and a library from the same release. The JVM finds the library through java.library.path:

java --enable-native-access=ALL-UNNAMED \
     -Djava.library.path=/path/to/library/directory \
     -jar app.jar

To name the file by absolute path instead, pass -Duniffi.component.offline_protocol.libraryOverride= with the path. The generated Java checks the library’s FFI contract version and every function’s checksum when it loads, so a mismatched library fails at the first call rather than misbehaving later. On a platform without a published library, cargo build --release -p offline-protocol-uniffi builds it from source.

Java quick start

A Java program creates the engine from a ProtocolConfig, sets up storage, then starts it. This is the engine part of the binding’s own quick start:

try (OfflineProtocol engine = new OfflineProtocol(config)) {
    // Storage before start(). The store key seals key material at rest;
    // keep it out of the source.
    engine.initializeMlsWithFileStores("/var/lib/app/mls", "/var/lib/app/state", storeKey32Bytes);

    engine.setEventCallback(json -> queue.add(json)); // copy and return; never call the engine from here
    engine.start();
    System.out.println("this device is " + engine.localAddress());

    try {
        engine.sendMessage("off1...", "hi", MessagePriority.MEDIUM, null);
    } catch (ProtocolException.EncryptFailed e) {
        // each engine refusal is its own exception type
    }
    engine.stop();
}

Java has no default arguments, so new ProtocolConfig(...) takes all 35 fields in order, and optional fields take null. The binding’s ConsumerCheckTest.config() carries the definition’s defaults field for field, which makes it the reference to copy from.

Events arrive as the engine’s JSON text, through setEventCallback or by calling pollEvent; every event is queued either way. A callback runs on whichever thread is inside the engine, must be safe from any thread and must not call back into the engine, closing it included. Copy the event, queue it and return.

Host responsibilities in Java

In Java your process is the platform, so it decides how frames move and where state lives. The engine handles routing, encryption and delivery, and the host connects it to the world:

  • Transports. Implement BleTransportCallback, WifiDirectTransportCallback, ReticulumTransportCallback and NostrTransportCallback to learn when a queue has frames, then drain them with the ble*, wifiDirect*, internet*, reticulum* and nostr* methods.
  • Storage. Implement MlsStorageProvider for key material, backed by a keystore, and ProtocolStateStorageProvider, and pass both to initializeMls. Prove an adapter with OfflineProtocolLib.runStorageConformance. A host with a filesystem can use the built-in file stores instead, as in the quick start.
  • Telemetry platform. If you enable telemetry, pass TelemetryConfig.os and osMajor as the platform the process actually runs on, read from the JVM’s os.name and os.version.
  • Lifecycle. Call start, stop, and process on a timer. try-with-resources frees the engine once; it does not wait for calls in flight on other threads.

This gives a JVM service full control over its carriers. For transports ready-made, the Python package ships Bluetooth LE, internet, LAN peer-stream and gateway bridges on desktop and server hosts, and on phones React Native is the most complete integration.

Runnable Java example

The SDK repository has a runnable Java program in examples/java-app, and since 0.31.0 CI runs it. Two devices, Alice and Bob, live in one JVM and:

  • pair from events, answering the connection request with a key package exchange;
  • form an end-to-end encrypted session and exchange presence, typing, a message, a read receipt and a threaded reply;
  • send a file that is checked byte for byte on arrival;
  • create an MLS group, invite the paired peer and send a group message.

The carrier between them is a dozen lines: take what one engine wants to send (internetGetNextMessage), hand it to the other (internetMessageReceived) and confirm the write (internetConfirmSent). A real app does the same over a radio or a socket, and the pairing and MLS encryption stay the engine’s.

To run it from a checkout, with JDK 22 or later and Gradle 9:

cargo build -p offline-protocol-uniffi      # from the repository root
cd examples/java-app
gradle run                                   # Gradle 9; -PnativeLibrary=<file> for another library

The binding’s own consumer check, ConsumerCheckTest, runs against the real library in CI on Linux, macOS and Windows. The SDK is AGPL-3.0-only, with a commercial licence for proprietary use.

Frequently asked questions

Does the Offline Protocol Java binding work on Java 17 or Java 21?

No. The generated code uses the foreign function API, which is final in Java 22, and it is compiled with --release 22, so an older JVM cannot load it.

Do I need JNA or JNI to use Offline Protocol from Java?

No. The binding calls the native library through java.lang.foreign. It has no JNA, no JNI and no native glue of its own, and it does not go through the C API.

Should an Android app use offline-protocol-java?

No. On Android use the Android library, com.offlineprotocol:offline-protocol-sdk, which is built on JNA and ships as an AAR. The Java jar is for any other JVM.

Does the Maven Central jar include the native library?

No. The jar holds the generated Java and the licence notices only. Download liboffline_protocol_uniffi for your platform from the same GitHub release and point the JVM at it.

Why does ProtocolConfig in Java take every field?

Java has no default arguments and the generator emits no overloads, so the constructor takes all 35 fields in order. When a release changes a default, your code keeps passing the old value until you change it.

Does the Java binding open Bluetooth or sockets for me?

No. Your process is the platform, so it owns the radios and sockets. The binding tells you when a transport queue has frames and you drain and deliver them.

Sources

Build it with Offline Protocol

The Java page of the docs has the Gradle line, the native library for each platform, the quick start and the list of what your host provides.

Read the Java binding docs