Sending a message with no server
The smallest useful thing you can build on Offline Protocol is a message that reaches its recipient with no server in the path. It is also the thing that best explains what the platform actually does, because everything else, identity, discovery, coordination, sits underneath that one send.
Here is the whole program in the React Native SDK.
// npm install @offline-protocol/mesh-sdk
import { OfflineProtocol, MessagePriority } from '@offline-protocol/mesh-sdk';
const protocol = new OfflineProtocol({ appId: 'my-app', userId: 'user123' });
protocol.on('message_received', (event) =>
console.log(`From ${event.sender}: ${event.content}`));
await protocol.start();
await protocol.sendMessage({
recipient: 'user456',
content: 'Hello!',
priority: MessagePriority.High,
});
There is no endpoint URL, no API key, no broker to connect to. That absence is the point. Let us walk through what each line sets in motion.
Start brings up the transports
protocol.start() does not open a socket. It brings up DORS, the transport layer, which runs BLE, WiFi Direct, and internet concurrently rather than choosing one. The Rust core ships inside the binding, so there is no separate native build step to reach that behavior. From this moment the device is a mesh node: it is discovering nearby peers, advertising itself, and standing ready to relay traffic for others.
Send routes across a portfolio, not a link
sendMessage hands the message to DORS, which treats connectivity as a portfolio rather than a binary state. If user456 is one hop away over BLE, the message goes directly. If they are further out, DORS relays across up to 8 hops, with acknowledgment, retry, and deduplication handled for you. If the internet is available and faster for this path, it uses that too. The application code does not change based on which transport carries the bytes, and it does not fail just because one of them is unavailable.
The priority field is the one lever the caller pulls into that decision. High-priority traffic is moved ahead of routine background sync when the local radios are contended, which matters when the same mesh is carrying both a person’s message and a fleet’s telemetry.
Delivery is encrypted by default
The recipient is named by their identity, not their address. Under the hood, sessions are encrypted with MLS (RFC 9420), so every message is protected without the caller opting in. This is why the recipient is 'user456' and not an IP or a device handle: the identity layer resolves who that is, and the session it establishes is the encrypted channel. You are not addressing a machine on a network, you are addressing a person or a service that the mesh knows how to reach.
Receive is an event, because arrival is not synchronous
The message_received handler is a subscription rather than a return value because, on a disconnected mesh, arrival is not synchronous. A message may hop through several relays and arrive seconds later, or after the recipient’s device comes back into range. Modeling receipt as an event keeps the application honest about that. You react when the message lands, rather than blocking on a request that assumes a live path to a server.
Why this shape matters
Most messaging SDKs are thin clients in front of a server that does the real work: routing, fan-out, delivery, presence. Offline Protocol pushes that work onto the devices themselves, so the same four calls keep working when there is no server to reach. The API stays small because the hard parts, multi-transport routing, relaying, encryption, and identity resolution, are handled beneath the one call rather than exposed as configuration.
If you want to build on it, that send is the right place to start. Everything the platform offers is reachable from there.