The internet has trained us to think of messaging as a service. A client sends data to an always-available server, the server finds the recipient, and both devices remain mostly unaware of the network between them. It is simple, scalable, and convenient—until the infrastructure disappears.
A power failure, disaster, overloaded mobile network, remote location, censorship event, or crowded venue can remove the assumptions behind ordinary chat. The phone still has a radio, storage, a battery, and people nearby, but the application becomes useless because it cannot reach its data center.
Bitchat, one of the most discussed open-source projects on GitHub, explores a different model. Nearby phones form an ad-hoc Bluetooth Low Energy mesh. Every device can discover peers, exchange compact packets, relay traffic, and help messages move beyond direct radio range. When the internet is available, a second transport uses Nostr relays to connect separated meshes and location-based channels.
The result is not “WhatsApp without servers.” It is a delay-tolerant, intermittently connected messaging system with a very different set of guarantees, risks, and design constraints.
This article examines those constraints: controlled flooding, TTL, fragmentation, deduplication, opportunistic couriers, encrypted sessions, identity verification, geohash channels, battery limits, metadata, and the uncomfortable gap between delivering a message and proving who received it.
Messaging without infrastructure changes the problem
Centralized messaging has a stable rendezvous point. Alice and Bob do not need to be online simultaneously or physically close. Each client knows where to connect, and the server maintains durable queues, identity records, abuse controls, and device state.
An offline mesh has none of that by default.
Alice may see Bob directly for thirty seconds, lose him, encounter Carol five minutes later, and reconnect to Bob through a chain of moving devices. A packet may arrive twice, out of order, or after its conversation context has expired. A device may vanish midway through a transfer. A malicious participant can replay old packets or inject thousands of new ones. Every relay spends someone else’s battery.
The design goal is therefore not perfect real-time delivery. It is useful communication under churn, loss, limited bandwidth, and partial connectivity.
That reframing is essential. An offline messenger should be evaluated like a delay-tolerant network, not like a cloud chat application with a strange transport.
The dual-transport architecture
Bitchat combines two complementary networks:
- a local Bluetooth Low Energy mesh for nearby, infrastructure-free communication;
- Nostr relays for internet-connected fallback, distant private delivery, and geographic channels.
A message router chooses a path based on reachability and relationship state. A nearby peer can receive traffic through an established mesh session. A distant favorite can receive an encrypted envelope through internet relays. If neither path is currently available, the sender can retain sealed content and retry later.
The transport interface can be imagined as:
protocol MessageTransport {
var isAvailable: Bool { get }
func canReach(_ peer: PeerID) async -> Bool
func send(_ envelope: Envelope, to peer: PeerID) async throws
func events() -> AsyncStream<TransportEvent>
}
final class MessageRouter {
let mesh: MessageTransport
let nostr: MessageTransport
let outbox: PersistentOutbox
func route(_ envelope: Envelope, to peer: PeerID) async {
if mesh.isAvailable && await mesh.canReach(peer) {
try? await mesh.send(envelope, to: peer)
} else if nostr.isAvailable && peer.supportsRelayMail {
try? await nostr.send(envelope, to: peer)
} else {
await outbox.store(envelope, peer: peer)
}
}
}
The code is simple; the semantics are not. “Sent” can mean placed on a radio link, accepted by a nearby relay, stored in an outbox, published to a relay, or decrypted by the final recipient. A trustworthy UI must not collapse those states into one check mark.
Every phone becomes both client and router
In a mesh, a device is not only consuming network service. It is providing it.
Suppose Alice can reach Carol, Carol can reach Dan, and Dan can reach Bob. Alice’s packet may travel through all three intermediate devices. Those devices do not need to understand the private message; they only need enough routing metadata to decide whether to forward it.
This turns ordinary phones into temporary network infrastructure. The topology changes as people move. A crowded station may have many potential paths for a few minutes. A rural road may have none. A person walking between disconnected groups can physically carry sealed traffic and become a bridge over time.
The network is therefore both spatial and temporal. Connectivity is not merely “who is in range now?” It is also “who might meet the recipient before this message expires?”
Controlled flooding is intentionally simple
Traditional networks invest heavily in route discovery and stable topology. A phone mesh cannot assume either. Peers appear and disappear too quickly, and maintaining global routes may cost more bandwidth than forwarding the messages.
Controlled flooding uses a simpler rule: when a device receives a packet it has not seen, it may relay the packet to its neighbors with a reduced hop limit.
fun onPacket(packet: MeshPacket, from: PeerId) {
if (packet.expiresAt < clock.now()) return
if (!dedup.markIfNew(packet.messageId)) return
deliverLocallyIfRelevant(packet)
if (packet.ttl > 0 && relayPolicy.allows(packet)) {
val forwarded = packet.copy(ttl = packet.ttl - 1)
neighbors.excluding(from).forEach { peer ->
radio.enqueue(peer, forwarded)
}
}
}
This design tolerates churn because it does not depend on a perfect route table. It also produces duplicates and consumes radio time. TTL, deduplication, rate limits, and queue discipline are what keep controlled flooding from becoming uncontrolled noise.
TTL is a resource budget, not just a routing field
Time-to-live is often explained as a loop-prevention mechanism. In a constrained mesh it is also an explicit budget for other people’s devices.
A packet with TTL 7 may cross at most seven forwarding steps. Each hop consumes airtime, connection slots, CPU, and battery. Increasing the value may improve reach but multiplies the number of potential transmissions in a dense network.
In the worst case, naive flooding grows roughly with the branching factor of the peer graph. Deduplication stops repeated processing of the same ID, but the first wave can still be expensive.
The right TTL depends on density, message priority, packet size, and the application’s social contract. An emergency alert might justify more reach than a typing indicator. A voice fragment should not receive the same forwarding treatment as a short text message.
Good meshes classify traffic before spending shared resources.
Stable message IDs make deduplication possible
If a packet can reach a phone along several paths, duplicates are normal. The receiver needs a stable identity that survives different relays and transport bridges.
type MessageIdentityInput = {
senderKey: string;
conversationId: string;
createdAt: number;
nonce: string;
ciphertextDigest: string;
};
function stableMessageId(input: MessageIdentityInput): string {
return sha256(canonicalJson(input));
}
The ID must be difficult for an attacker to collide deliberately and stable enough that Bluetooth and internet copies are recognized as the same logical message.
A dedup cache also needs an expiration policy. Keeping every ID forever is impossible. Evicting too early allows old packets to circulate again. The retention window should cover the longest permitted store-and-forward lifetime plus clock uncertainty.
Probabilistic structures such as Bloom filters can reduce memory, but false positives may drop a real message. In a social feed that may be acceptable. In an emergency channel it may not be.
Bluetooth Low Energy shapes the packet format
BLE was designed for power-efficient peripheral communication, not large conversational payloads. Packet sizes are small, connections are limited, and mobile operating systems aggressively manage background activity.
A mesh protocol therefore benefits from a compact binary frame rather than verbose JSON on the radio.
+---------+---------+----------+---------+----------+---------+
| version | type | flags | ttl | sequence | length |
+---------+---------+----------+---------+----------+---------+
| sender-id | message-id |
+---------------------------------------------------+
| payload fragment |
+---------------------------------------------------+
Versioning allows protocol evolution. Type separates announcements, data, acknowledgments, and control frames. Flags can indicate compression, encryption, or fragmentation. Sequence information allows the receiver to rebuild a larger envelope.
Binary efficiency is useful, but compact formats create compatibility risk. Every integer width, byte order, length check, and unknown flag must be specified. A parser operating on hostile radio input must reject malformed frames without allocating attacker-controlled amounts of memory.
Fragmentation turns one message into a small distributed system
Images, voice notes, and even long encrypted messages exceed a practical BLE frame. They must be fragmented.
struct FragmentHeader {
message_id: [u8; 32],
index: u16,
total: u16,
payload_len: u16,
}
fn accept_fragment(fragment: Fragment) -> Result<Option<Vec<u8>>, Error> {
validate_bounds(&fragment.header)?;
let assembly = assemblies.get_or_create(fragment.header.message_id)?;
assembly.insert(fragment.header.index, fragment.payload)?;
if assembly.is_complete() {
Ok(Some(assembly.reassemble_and_verify()?))
} else {
Ok(None)
}
}
The receiver must bound the number of incomplete assemblies, maximum declared fragment count, total bytes, per-peer quota, and lifetime. Otherwise an attacker can send the first fragment of thousands of imaginary media files and exhaust memory or disk.
Private media should be encrypted before fragmentation. If encryption happens afterward, intermediate relays see plaintext chunks. Encrypt-first also allows the final digest or authentication tag to verify the reassembled ciphertext as one object.
Cancellation matters too. If the user deletes a pending transfer, every queue and temporary file associated with it should be removed. Partial content is still sensitive content.
Store-and-forward accepts that sender and recipient may never overlap
Direct mesh delivery works when a path exists at the time of sending. Real intermittent networks need a longer memory.
Bitchat’s documented model layers several mechanisms: a sender outbox, opportunistic couriers, gossip for public history, and relay mailboxes when internet connectivity appears.
The key insight is that mobility can substitute for a continuous path. A courier device carries opaque ciphertext while moving and delivers it when it encounters another useful peer.
This resembles delay-tolerant networking used in environments where end-to-end connectivity cannot be assumed.
def consider_carry(message, peer, now):
if message.expires_at <= now:
return False
if message.recipient == peer.identity:
return True
if message.copy_budget <= 0:
return False
if peer.has_seen(message.id):
return False
return peer.delivery_score(message.recipient) > MIN_SCORE
The delivery_score might use recent encounters, neighborhood membership, or simple randomness. Every heuristic leaks or infers something about social movement, so the most private design may deliberately remain less efficient.
Spray-and-wait limits epidemic replication
Pure epidemic routing gives every encountered device a copy. Delivery probability can be high, but storage and bandwidth usage explode.
Spray-and-wait assigns a finite copy budget. The sender distributes a limited number of replicas. Couriers then hold them until they meet the recipient or a better carrier.
Initial copy budget: 8
Sender -> Courier A : 4 copies remain with sender, 4 with A
Sender -> Courier B : 2 copies remain with sender, 2 with B
Courier A -> Courier C: 2 remain with A, 2 with C
All holders then wait for a useful encounter.
This trades some delivery probability for predictable resource consumption. It also creates policy questions: should high-priority messages receive a larger budget? Can an attacker label everything urgent? Do couriers learn the destination identity? How are expired copies destroyed?
There is no free reliability. Every additional copy improves reach by consuming someone else’s resources and increasing observable metadata.
Public messages and private messages need different semantics
A public mesh channel is naturally gossip-like. Repetition helps late joiners reconstruct recent context. Authenticity and spam resistance matter, but confidentiality is not the goal.
A private message is different. Intermediate relays should carry opaque ciphertext. The recipient must verify the sender, and delivery metadata should be minimized. Forwarding a private envelope should not require decrypting its content.
Trying to force both modes through identical rules leads to mistakes. Public history may be cached for convenience, while private conversation timelines may be ephemeral. Public messages can tolerate content-based deduplication. Private messages should avoid identifiers that reveal linkable plaintext properties.
Protocol uniformity is useful only until the security goals diverge.
Noise sessions protect live private mesh conversations
The project uses the Noise Protocol framework for private sessions over the mesh, with modern elliptic-curve key agreement and authenticated encryption primitives.
Conceptually, an XX-style handshake allows two peers without pre-shared session state to establish encryption while exchanging and authenticating static identities during the handshake.
Initiator Responder
| |
| -------- ephemeral key ------------> |
| <--- ephemeral key + encrypted ID -- |
| ----- encrypted static ID ---------> |
| |
| === authenticated session keys ==== |
The actual security depends on binding the handshake to the identity the interface claims. If a peer can complete a valid encrypted session while claiming another person’s visible identifier, confidentiality exists but authentication fails.
Recent security work in the project explicitly tightened session-to-identity binding and signed leave behavior. That history illustrates an important lesson: using a respected cryptographic framework does not automatically make the surrounding identity protocol correct.
Forward secrecy has boundaries
“Encrypted” is not a single property. A live Noise session may provide forward secrecy for session material, while a store-and-forward envelope encrypted to a long-term static recipient key may not protect old ciphertext after that key is later compromised.
The transport matters too. A private message sent through an active mesh session can have different guarantees from one deposited in a relay mailbox for an offline recipient.
A responsible interface should avoid one universal lock icon if the modes have materially different properties. Security documentation should answer:
- Which key encrypts the payload?
- When is that key erased?
- Can a later device compromise decrypt recorded traffic?
- Can a courier link sender and recipient?
- Does the recipient authenticate the same identity shown in the UI?
- Are media and text protected before fragmentation and storage?
Precise language is a security feature.
Identity without accounts shifts trust to keys and verification
No email address or phone number is required. That removes a central account provider, but it does not remove identity. Identity moves to cryptographic keys stored on the device.
Nicknames are not proof. Anyone can choose “Alice.” A fingerprint or QR-based verification lets two people confirm keys through a separate physical channel. Trust on first use can remember the first observed key and warn when it changes.
TOFU is practical, but it detects change rather than initial impersonation. If an attacker is present during the first contact, the user may pin the attacker’s key.
Device-bound identity keys also create a usability trade-off. Preventing cloud backup extraction limits certain attacks, but reinstalling the app or moving to a new phone may create a new identity. Users need a clear explanation that “same nickname” does not mean “same cryptographic person.”
Metadata survives excellent encryption
End-to-end encryption hides content from intermediate nodes. The radio environment still exposes signals.
A nearby observer may learn that a device participates, when it transmits, how frequently it sends, which packets are related, and how identifiers persist over time. Relay operators may observe publication times, event sizes, network addresses, and subscribed geographic cells. Couriers may see delivery deadlines and routing identifiers even when content is opaque.
Persistent per-device identifiers improve deduplication and relationships but can also support tracking by nearby radios. Rotating identifiers improve privacy but complicate recognition, routing, abuse control, and favorite relationships.
This is a classic systems trade-off: stable identity helps the protocol function; unlinkability helps the user disappear.
The correct choice depends on the threat model. A festival chat, disaster network, protest communication tool, and family messenger do not face the same adversary.
Geohash channels turn location into a routing key
Internet-connected location channels use geohashes: strings that represent geographic cells at different levels of precision. Short strings cover broad regions; longer strings cover smaller areas.
type GeoChannel = {
geohash: string;
precision: number;
};
function chooseChannel(latitude: number, longitude: number, meters: number) {
const precision = meters < 200 ? 7
: meters < 1000 ? 6
: meters < 10000 ? 5
: 3;
return {
geohash: encodeGeohash(latitude, longitude, precision),
precision,
};
}
The precise coordinates can remain on the device while the derived cell becomes the channel identifier. That is better than publishing raw GPS, but the cell is still location information. A block-level geohash may reveal much more than a user expects from the phrase “local chat.”
Subscription behavior also matters. If a client keeps old geohash subscriptions alive as the person moves, a relay can observe a path across cells. Cleanup is a privacy boundary, not merely a performance optimization.
Relay selection creates a hidden delivery dependency
Nostr is decentralized in the sense that many relays can carry events. That does not mean every publisher and subscriber automatically meet.
If a geographic channel selects relays based on proximity to the cell, an external sender using a fixed relay set may publish a valid event that no intended client sees. Sparse regions may map to unreachable infrastructure. Ephemeral events may disappear before a late subscriber connects.
The delivery contract should therefore distinguish protocol validity from practical reachability.
valid event + wrong relay set = no delivery
valid event + late subscription = no history
valid event + sparse region = uncertain reach
valid event + matching relay = possible delivery
Decentralized systems often replace one reliable rendezvous point with a discovery problem. That may be the right trade, but it should be visible to developers and users.
Bridging transports creates duplicate and ordering problems
The same logical message can travel through Bluetooth, a courier, and a Nostr relay. The recipient may receive the internet copy first and the mesh copy later.
Cross-transport message IDs should allow deduplication without exposing plaintext. Delivery state should merge monotonically: receiving an older transport event must not move the UI backward from “decrypted” to “relayed.”
const rank = {
queued: 0,
relayed: 1,
delivered: 2,
decrypted: 3,
};
function mergeStatus(a: Status, b: Status): Status {
return rank[a] >= rank[b] ? a : b;
}
Ordering is also ambiguous when devices have inaccurate clocks. A conversation should prefer causal sequence numbers or signed sender counters where possible, then use timestamps as presentation hints rather than unquestionable truth.
Battery is part of the protocol
Every peer discovery scan, advertisement, connection, fragment transfer, retry, and relay consumes energy. Mobile operating systems also restrict background radio activity to protect users.
An adaptive mesh can change behavior based on battery, charging state, foreground state, network density, and message priority.
fun relayMode(state: DeviceState): RelayMode = when {
state.batteryPercent < 10 -> RelayMode.RECEIVE_ONLY
state.isCharging -> RelayMode.HIGH_AVAILABILITY
state.peerCount > 30 -> RelayMode.DUTY_CYCLED
state.appInForeground -> RelayMode.INTERACTIVE
else -> RelayMode.BALANCED
}
This makes performance nondeterministic. A path that worked while several devices were charging may disappear when they enter low-power mode. The UI should treat availability as probabilistic, not promise a permanent mesh simply because peers were visible once.
Backpressure protects the network from one loud participant
A malicious or buggy peer can advertise constantly, send oversized fragment sets, generate unique message IDs to defeat deduplication, or fill courier queues with undeliverable traffic.
Defenses need to exist at several layers:
- maximum frame and envelope sizes;
- bounded incomplete assemblies;
- per-peer and global queues;
- token-bucket rate limits;
- priority classes;
- expiry enforcement;
- proof-of-work or admission costs for public channels;
- disconnect and quarantine behavior for repeated violations.
fn admit(peer: PeerId, packet: &Packet, limits: &mut Limits) -> bool {
limits.global_bytes.try_take(packet.len()) &&
limits.per_peer(peer).try_take(packet.len()) &&
packet.declared_size <= MAX_MESSAGE_SIZE &&
packet.fragment_count <= MAX_FRAGMENTS &&
packet.expires_at <= now() + MAX_RETENTION
}
Resource limits are security controls. In a volunteer mesh, exhausting batteries may be more damaging than crashing one process.
Panic wipe is a lifecycle guarantee
An emergency wipe feature sounds like a UI gesture. Technically it is a data-lifecycle contract.
The application may hold identity keys, favorite relationships, sealed outbox messages, courier copies, cached public history, media, thumbnails, partial fragments, database journals, and operating-system snapshots. Deleting the visible conversation is not enough.
A deterministic wipe needs an inventory of every storage location, clear behavior when the device is locked, and tests that fail when new persisted state is added without a deletion path.
Flash storage complicates claims of physical erasure. Secure deletion often relies on destroying encryption keys rather than overwriting every cell. Device backups and cloud synchronization must not quietly restore the supposedly destroyed identity.
The honest promise is usually “make application data inaccessible through supported device mechanisms,” not “prove that no forensic trace can ever exist.”
Building a copy you can trust is part of the threat model
Open-source code does not guarantee that a downloaded binary corresponds to that code. A malicious mirror can publish an application with the same name and entirely different behavior.
This risk becomes acute when a project is censored, removed, or copied. Users searching for an emergency messenger are unusually vulnerable to convincing replicas.
Reproducible build procedures, source manifests, signed release commits, published artifact hashes, and multiple independent verification channels help close the gap.
source tree -> deterministic build -> artifact digest
| |
signed tag published manifest
| |
+---------- user verifies ---------+
Verification is only as strong as the key-distribution story. A hash copied from the same compromised download page proves little.
A disaster messenger should fail honestly
During an emergency, ambiguous status can be dangerous. “Sent” should not imply that a recipient saw a message. A peer icon should not imply a stable end-to-end path. A green lock should not hide differences between live session secrecy and store-and-forward envelopes.
Useful states might include:
- stored locally;
- offered to nearby mesh;
- accepted by one or more couriers;
- published to relay fallback;
- received by recipient device;
- decrypted and acknowledged.
Not every mode can provide every state. The UI should expose uncertainty rather than invent confirmation.
The same applies to availability. Offline mesh communication depends on enough participating devices, radio permissions, operating-system scheduling, and physical movement. It is a resilience tool, not a replacement for all emergency planning.
What developers can learn from Bitchat
The project offers lessons beyond messaging.
Design for partitions as a normal state. Connectivity may return later, and the system should preserve useful work until it does.
Make identity and delivery semantics explicit. Encryption, authentication, reachability, and acknowledgment are different guarantees.
Bound every attacker-controlled dimension. Packet size, fragment count, queue length, retention time, subscription count, and copy budget all need limits.
Treat metadata as data. Hiding message content does not hide participation, timing, movement, or social relationships.
Use boring cryptographic primitives, then review the surrounding protocol even more carefully. Most failures occur in identity binding, key lifecycle, serialization, and UI claims.
Prefer graceful uncertainty over false certainty. Intermittent systems should say “not known” when delivery is not known.
The larger significance of offline-first communication
Bitchat is interesting not because Bluetooth mesh will replace internet messaging. It will not. Central servers remain far more efficient for global discovery, durable delivery, moderation, backup, and multi-device synchronization.
The project matters because it treats infrastructure failure as a first-class condition. It asks what a phone can still do when the cloud is unavailable and nearby people are the network.
That question applies to more than chat. Local coordination, disaster reporting, event communication, field research, logistics, and community networks can all benefit from systems that degrade into peer-to-peer operation instead of becoming blank screens.
The engineering discipline is the real story: compact protocols, controlled replication, explicit expiry, multiple transports, cryptographic identity, bounded resources, and honest delivery states.
Final thought: resilience is a stack of imperfect paths
No single path in Bitchat is sufficient. Direct Bluetooth has short range. Multi-hop mesh depends on density. Couriers introduce delay. Nostr fallback needs internet access and compatible relay discovery. Persistent outboxes need secure local storage. Encryption protects content but not all metadata.
Together, those imperfect paths create something more resilient than any one of them.
That is the deeper design pattern. Resilience rarely comes from making one channel infallible. It comes from combining independent channels, making their guarantees visible, limiting their failure modes, and allowing useful state to survive while the network changes around it.
Top comments (0)