BLE Off-Grid Messaging: Briar, Knit, bitchat, and Columba Compared

There's a small cluster of open-source apps that promise to let you message people without cell service, wifi, or a server anyone controls, using nothing but the radios already in your phone. They bounce encrypted messages phone-to-phone over Bluetooth, or hold onto them in a dead-drop until someone can pick them up. I'd seen several of them mentioned around enough times that I wanted to actually read the code instead of the marketing copy: Briar, Knit, and bitchat, which ships as two sibling implementations. One is Swift for iOS/macOS, one is Kotlin for Android, and they share a wire protocol but not always the same behavior. I also spent some hands-on time with Columba, an Android client for the Reticulum/LXMF stack, which takes a fundamentally different architectural approach from the other four and gets its own section below rather than a code read-through.

To be clear about scope: this is about apps that turn an ordinary phone's BLE radio into the mesh, not about dedicated LoRa hardware mesh networks like Meshtastic. Meshtastic solves a different problem (long-range radio relay between purpose-built nodes, phone optional) and deserves its own comparison rather than getting lumped in with "install an app and go."

They're all solving the same problem: off-grid messaging without central infrastructure. But the actual engineering underneath is surprisingly different. Here's what I found going through background execution, offline delivery, mesh transport, and crypto in each.

Quick reference #

BriarKnitbitchat (iOS)bitchat-androidColumba (Reticulum/LXMF)
Transport modelPairwise, trust-graph (BLE, LAN, Tor, USB plugins)Flood mesh (BLE + Wi-Fi Aware)Flood mesh (BLE only)Flood mesh (BLE + Wi-Fi Aware)Reticulum routed mesh (BLE tested; also does TCP, LoRa/packet radio)
Offline deliverySelf-hosted Tor "mailbox" dead-dropDB-backed store + anti-entropy digest syncSender outbox + physical/virtual couriers + NostrIn-memory cache + Nostr fallbackLXMF propagation nodes (opt-in store-and-forward)
HandshakeBQP (Curve25519 DH + commitment)X3DH-style (3x X25519 DH)Noise XXNoise XXX25519 ECDH + Ed25519 identity (Reticulum Link)
Forward secrecyPer-transport-time-period key rotationPer-epoch ratchet (~200 msgs/24h)Per-message (live), weaker offlinePer-message (live)Per-link (ephemeral, renegotiated per Link)
PlatformAndroid + desktop headlessAndroid onlyiOS/macOSAndroidAndroid

Off-grid doesn't have to mean mesh #

Worth getting this out of the way first, since it shapes everything else. These four split into two transport models for the same underlying goal.

Knit and both bitchats are genuine flood/relay meshes. Your phone forwards other people's traffic, hopping messages across strangers' radios until they reach their target. Briar isn't built that way. Every connection it makes, whether Bluetooth, local wifi, Tor, or even a USB drive, goes directly to a specific contact you've already added, and nothing relays through a stranger's device. Bluetooth and LAN plugins handle contacts who are physically nearby without needing any internet path at all; Tor is there for contacts who aren't, giving Briar reach without ever hopping through a stranger's radio, plus a gossip mechanism for forum and group content that spreads only across your existing mutual contacts. Both are legitimate answers to off-grid messaging without a server; they just make opposite trade-offs between reaching strangers and keeping the trust circle small.

Staying alive in the background #

Nobody's found a loophole around Android's rules here. Briar, Knit, bitchat-android, and Columba all converge on the same shape: a foreground Service, a persistent low-priority notification, and a prompt asking the user to exempt the app from battery optimization. The differentiation is in how smart each one is about when to actually burn radio power scanning, and in Columba's case, in how the service itself is structured.

bitchat on iOS doesn't get any of these levers. Apple's background BLE rules are stricter across the board, so the whole story is CoreBluetooth's built-in state-restoration API (CBCentralManagerOptionRestoreIdentifierKey and friends), which lets the app rebuild its Bluetooth link state after iOS suspends and relaunches it in the background. There's no Android-style fine-grained scheduling available to reach for.

Reaching someone who isn't there right now #

This is where the four diverge the most. Two mechanisms are worth sitting with in particular: Briar's mailbox and bitchat's courier system.

A Briar mailbox is small self-hosted infrastructure, like a spare phone or a cheap VPS, running as a Tor hidden service that a user sets up themselves as a drop-box contacts can leave messages in. The implementation is neat: it's literally just another SimplexPlugin, reusing the same FilePlugin base class as Briar's removable-USB-drive transport. Each contact gets an isolated inbox/outbox folder pair and a private auth token that grants folder access only, never decrypt or sign capability. So the mailbox operator sees rough timing and blob sizes and nothing else. It can go offline and stall your delivery, but it can't read or forge anything.

bitchat's courier mechanism solves the same "recipient isn't reachable right now" problem in a completely different way. Any nearby phone running bitchat can carry a sealed, opaque envelope on behalf of someone it might later run into, with a spray-and-wait copy budget (starting at up to 8 copies, halved on each handoff) that substitutes redundancy across strangers for Briar's one-chosen-relay reliability. It's a genuinely inventive idea: it can deliver between two devices that were never online at the same time, with zero setup required. But the project's own docs (docs/PEER-ID-ROTATION.md) admit a real flaw. The day-rotating tag couriers use to figure out who an envelope is for is an HMAC over the recipient's public key, and that key gets broadcast in cleartext in every announce packet. So any passive listener, not just an actual courier, can precompute and correlate a peer's tags across days. Content integrity in both systems is equally solid; a tampered message just fails to decrypt in either design, with no silent corruption. The real difference is that Briar's relay is infrastructure you specifically chose, while bitchat's is an anonymous population of strangers' phones, and that choice is exactly what determines how contained the metadata leakage stays.

Durability varies a lot too. Knit's forward_store (Room + SQLCipher) and bitchat-iOS's outbox both persist to encrypted disk and survive a restart. bitchat-android's StoreForwardManager currently caches everything in plain ConcurrentHashMaps, so force-closing the app wipes out whatever was queued. Knit's redelivery mechanism is the most sophisticated of the four: instead of re-flooding cached messages at a newly-seen neighbor, it exchanges a short digest of held message IDs and only sends the delta. That's an actual anti-entropy sync rather than a blind re-push, and it means a mesh that's already converged does close to zero extra work.

Would any of this hold up in a crowd? #

None of these projects publish load-test numbers, so take this section as reasoning from the code and known radio constraints, not a measured result.

The real ceiling is almost certainly Bluetooth's connection-count limit, not any relay algorithm. A BLE central can only hold a handful of GATT connections open at once (bitchat-android's code caps it explicitly at 8), so a crowd of a few hundred phones fragments into many small, overlapping clusters no matter how clever the software is. That's the actual reason multi-hop relay exists at all.

At small scale, a handful to a few dozen devices in one cluster, Knit, bitchat-iOS, and bitchat-android should all behave comparably. There isn't enough redundant chatter yet for their different rebroadcast-suppression strategies to matter. They diverge more in the tens-to-hundreds range, a regime all three explicitly designed for. bitchat-android's own decay curve names both a ≤10 and a >100 peer threshold directly in the code.

Wi-Fi Aware (used by Knit and bitchat-android, not iOS) helps throughput for a given exchange, which is useful for attachments. But its data-path negotiation is one-active-exchange-at-a-time in Knit's implementation, so it doesn't raise how many simultaneous peers a device usefully meshes with. Whichever relay algorithm wins on paper, my bet is that the actual bottleneck at real crowd scale is BLE connection fragmentation, a limit common to all three, well before any of these suppression strategies start mattering.

Briar sidesteps most of this class of problem, since it never relays through a stranger's radio in the first place. It isn't entirely immune to BLE's connection-count ceiling though: polling many contacts over its Bluetooth plugin in one place still competes for the same handful of GATT connection slots as the flood meshes, it just doesn't get worse as strangers pile in the way a flood mesh does. Its real scaling limits are the number of contacts one device polls and how much load a mailbox server can take, both ordinary infrastructure problems on top of that baseline radio constraint, rather than radio-physics problems on their own.

Columba doesn't fit cleanly into this comparison, and I still don't have a load-test basis to rank it. But it's worth being precise about what I do and don't have the code for: Columba's own repo doesn't contain this logic at all. The flood/path-discovery behavior lives in reticulum-kt, a separate dependency it pulls in for the actual Reticulum Transport implementation. Reading that code, its announce rebroadcast control isn't neighbor-count-aware the way the other three's are at all. A node only re-forwards an announce when the hop count actually increased from what it already has on file, i.e. a genuinely new or longer path; if it sees the same hop count again (bouncing back on a shared medium), it counts that as a "local rebroadcast" and gives up after 2 (LOCAL_REBROADCASTS_MAX), with a random jitter window before forwarding at all (PATHFINDER_RW, 0.5s) and a hard 128-hop ceiling (PATHFINDER_M). That's a path-vector-style dedup, not a crowd-density throttle, and these constants are a straight port of upstream Reticulum's Transport.py, not something Columba or its dependency invented for BLE specifically. Whether that translates to sane behavior on a BLE link shared by a few hundred strangers is a real open question I can't answer from source alone, since nothing here is tuned to BLE's connection-count ceiling the way bitchat-android's decay curve explicitly is.

Security, briefly #

All four get the primitives right: modern AEAD ciphers, X25519/Curve25519 agreement, Ed25519 signatures, nothing home-rolled. Where they differ is forward-secrecy granularity, which turns out to be a spectrum shaped by each project's delivery model rather than a simple better/worse ranking.

Every one of those forward-secrecy bullets is describing private messages, and it's worth being explicit that this is a distinct question from whether a message is encrypted at all. I've since gone and built a wire-compatible bitchat client from scratch (reverse-engineering the actual binary protocol byte-by-byte, not the whitepaper prose), and one thing that surprised me: bitchat's default experience, the public mesh chat every nearby stranger sees by just opening the app, is completely unencrypted. The wire payload for a public message or announce packet is raw UTF-8 with no cipher applied at all; the only cryptographic ingredient is an Ed25519 signature over the packet, which authenticates who sent it, not who can read it. Anyone with a BLE radio in listening range can read every public message and every announce (nickname, static Noise/signing public keys) without decrypting anything, on both iOS and Android. None of the forward-secrecy machinery above touches this path at all, it only ever applies once you drop into a 1:1 DM and a Noise XX handshake completes. That's a reasonable design choice for a public local chat, but it's easy to read "Noise XX, per-message forward secrecy" in a comparison table and assume it covers the whole app rather than just its private-messaging half.

Two more things are worth flagging, since they're the kind of detail you only find by reading code instead of README files. First, bitchat's own whitepaper is refreshingly blunt that "metadata is the weakest part of this design, and the peer ID does not help." The sender ID is derived from a static key hash and never rotates, so a passive listener can track a device across locations over time; there's a drafted fix with test vectors that simply isn't shipped yet. Second, bitchat-android's README claims Argon2id for channel-password derivation, but the actual code uses PBKDF2-HMAC-SHA256 at 100k iterations. That correctly matches iOS, it just doesn't match what the README says it does. Worth checking the code, not the marketing, before trusting any specific claim from a project like this.

A third thing, also only visible from the code: bitchat's Nostr fallback path (used to reach a mutually-favorited contact over the internet when there's no BLE path between you) labels its encryption "NIP-44 v2" on the wire, but it isn't the standard NIP-44 that the rest of the Nostr ecosystem implements. The published NIP-44 spec calls for ChaCha20 plus a separate HMAC-SHA256 authentication step, with the plaintext padded into fixed length buckets specifically to stop message-length fingerprinting. bitchat-android's implementation (NostrCrypto.kt) instead runs XChaCha20-Poly1305 as a single AEAD call, with no plaintext padding whatsoever, and its own code comments admit it: "Match iOS: derive HKDF input from the compressed shared point," describing a key-derivation step that isn't in the public spec either. Functionally this still gets you confidentiality and authenticity between two bitchat installs, but it means bitchat's Nostr DMs are opaque to (and unreadable by) any standard Nostr client, and they skip the length-hiding padding a spec-compliant NIP-44 implementation would apply. If part of the appeal of the Nostr fallback is "falls back to the wider Nostr network," that's not quite what's happening; it falls back to Nostr relays as a transport, but only other bitchat clients can actually read what gets sent.

A fifth data point: Columba and Reticulum/LXMF #

Everything above came from reading source. This section didn't; it's from actually trying to use Columba, an Android messaging client built on Reticulum (a general-purpose cryptographic networking stack) and LXMF (the message format and store-and-forward layer that rides on top of it). Worth being upfront that the confidence level here is lower than a code read, this is what happened when I actually tried it, not an audit of why.

Reticulum is architecturally the odd one out of the five. It's interface-agnostic by design: the same Reticulum identity and the same LXMF message can move over TCP/IP, LoRa or packet radio, or BLE, and the stack picks whatever interfaces are configured rather than being purpose-built around one radio. That's a genuinely different model from the other four, which are BLE-first mesh apps that bolt on a secondary transport (Wi-Fi Aware, Nostr) as a supplement. Reticulum treats the radio layer as a plugin, which is powerful, but it also means the thing you're testing isn't always the thing you think you're testing.

That showed up directly when I tried to test BLE specifically. I disabled every other interface in Columba's settings to force BLE-only operation, and it still found a path to a contact through what had to be a TCP/network interface underlay, since there is no realistic scenario where 8 intermediate multihop BLE nodes exist in my area. Reticulum's transport layer will happily route over whatever interface actually has a path, config intent aside, so a BLE-only test can end up silently riding a Wi-Fi or local network link instead. That's a real usability trap for anyone trying to evaluate BLE mesh performance specifically rather than Reticulum's networking as a whole.

Two other things stood out. First, there's no global or public chat the way bitchat has its unencrypted public mesh channel or Briar has forums; Columba's model is pairwise LXMF messaging and propagation-node store-and-forward, not a broadcast room. That's a legitimate design choice, not a bug, but it's a real functional gap if what you want is bitchat's "open the app, see what's nearby" experience. Second, when I did try a genuine BLE-only path, it simply couldn't find my other node after 5 minutes, and I gave up. I can't say from the outside whether that's a BLE interface immaturity issue, a discovery/announce-interval problem, or something specific to my test setup, but it meant I couldn't get a real BLE mesh test to complete at all, which is itself the finding.

Net: on the metric this post actually cares about, phone-to-phone BLE messaging, Columba's BLE story isn't in the same place as the four purpose-built BLE mesh apps above. No public chat, a BLE-only test that didn't stay BLE-only, and a discovery that just didn't complete in five minutes of trying. Don't go in expecting a bitchat or Knit equivalent.

Where that leaves things #

Briar is the most institutionally mature of the four I read in full. Dagger DI, a database migration chain running to nearly 50 versions, and reproducible Docker builds so published APKs can be verified against source all point the same way: it gets there by deliberately keeping its trust surface small, pairwise contacts only, no ad-hoc relay through strangers. Knit is the newest and, in some ways, the most cryptographically deliberate, with real thought put into the specific hard problem of combining forward secrecy with storage that has to evict old messages. bitchat's two versions are the most ambitious in scope. Physical message-carrying couriers are a genuinely novel idea, and they're paired with the most publicly candid self-assessment of the group, admitted weaknesses included. None of these are better or worse versions of the same app. They're four different, defensible answers to what off-grid messaging should trade away. Columba, on the BLE experience I actually tested, isn't a fifth answer in that same league: no global chat, a "BLE-only" test that quietly rode a TCP path instead, and a node discovery that just never completed.

Published