Skip to content
← All posts
8 min read

How BitChat works without a central chat server

A packet starts on Bluetooth, crosses up to seven hops, waits in encrypted storage when the route breaks, and uses Nostr only when the internet returns.

Inside BitChat's BLE flood control, Noise sessions, courier mail, Nostr fallback, and failure modes.

Take the internet away from two phones in a crowd. BitChat can still move a private message if Bluetooth can find a path between them. Nearby phones become relays, the packet carries a hop limit, duplicate copies are dropped, and the recipient opens ciphertext that the relays could not read. If the path breaks, the sender queues the message or hands an encrypted copy to a moving phone.

It is two transports behind one router

The current BitChat protocol whitepaper describes two transports behind a common MessageRouter. Bluetooth Low Energy handles local, off-grid communication. Nostr relays handle distant delivery and geohash channels when internet access exists. The router prefers a live mesh link, falls back to Nostr for eligible peers, and uses queued or courier delivery when neither route is ready.

The local mesh, delayed delivery, and internet path carry the same user action through different failure conditions.

During an internet shutdown, the Nostr side is gone. Local mesh chat and physical courier delivery are the useful paths. This is why BitChat can keep working around a protest site but cannot instantly reach any phone in another city without an internet route or a chain of moving peers.

Every phone scans and advertises

A BitChat device acts as both a Bluetooth GATT central and peripheral. It scans for nearby BitChat advertisements, advertises its own service, opens links to peers, and exchanges compact binary packets. The packet header carries a protocol version, message type, time-to-live, timestamp, flags, sender ID, optional recipient ID, payload length, and optional signature.

Large payloads do not fit in one Bluetooth transfer. Version 2 splits them into fragments of roughly 469 bytes, then reassembles them on receipt. Each device caps concurrent assemblies and total size so an unknown peer cannot ask it to hold unlimited partial messages.

The mesh is a controlled flood

A naive mesh would resend every packet to every link. That burns battery and creates loops. BitChat starts packets with a time-to-live of seven, decrements it at each hop, and changes its behavior based on how crowded the local graph is. Dense nodes cap broadcast depth at five. Thin chains can use the full incoming depth.

if seen(packet.id) or packet.ttl == 0:
    drop(packet)
else:
    remember(packet.id, for: 5 minutes)
    packet.ttl -= 1
    relay(packet, after: random(10ms...220ms))

That pseudocode is the core loop, not the literal Swift. A 1,000-entry seen set drops duplicates for five minutes. Random relay jitter gives another copy time to arrive first and cancel the scheduled send. Broadcasts use a deterministic subset of links, roughly the base-2 logarithm of the connection count, while handshakes, private packets, fragments, and sync traffic use wider or directed delivery.

Signed peer announcements include a small view of direct neighbors. When the app sees a recently confirmed path, it can source-route a packet along that path. If the route fails, it falls back to flooding.

The protocol has four ways to wait

A protest crowd is not a fixed network. People walk away, phones sleep, links disappear, and separate groups merge later. BitChat's store-and-forward layer handles that churn in four ways.

  1. The sender outbox keeps up to 100 private messages per peer for 24 hours. It retries on reconnect, stops after eight attempts, and clears a message after delivery or read acknowledgement.
  2. Courier mode seals a message for the recipient and gives copies to as many as three nearby peers. A courier can physically move between disconnected groups and hand the ciphertext onward.
  3. Public-history sync caches up to 1,000 broadcast packets and reconciles missing items about every 15 seconds. Public messages remain syncable for six hours.
  4. Nostr mailboxes hold gift-wrapped private messages for mutual favorites. A reconnecting client asks relays for the previous 24 hours.

Courier mode uses a spray-and-wait budget instead of uncontrolled copying. An envelope starts with a copy budget of four, capped at eight. When one courier meets another eligible courier, it hands over half its remaining budget. Rotating recipient tags let a courier match an envelope to a recipient it encounters without carrying a stable public address. A malicious courier can still drop the message, so redundancy reduces loss but cannot force delivery.

Private messages have three encryption paths

Each device holds a long-term Curve25519 key for Noise key agreement and an Ed25519 key for signatures. On first contact, two live peers use the Noise XX pattern with Curve25519, ChaCha20-Poly1305, and SHA-256. XX works when neither side knows the other's static key in advance, and the resulting session gives private messages forward secrecy.

An offline courier cannot run an interactive handshake with an absent recipient. BitChat instead uses one-way Noise X sealing to the recipient's static key. The courier sees ciphertext, but this path gives up forward secrecy: compromise of the recipient's static key can expose sealed mail that has not yet been delivered. The whitepaper lists prekeys as future work.

The internet path uses Nostr NIP-17 and NIP-59 gift wrapping. A private message becomes an encrypted event wrapped under a throwaway key before it reaches public relays. Mesh broadcasts are different. They are signed public traffic, not secret group messages. A user should treat anything said in a public mesh or geohash channel as public.

BitChat also has a panic wipe that removes local identity keys, favorites, queued mail, carried envelopes, cached public history, and local metrics. A wipe cannot delete copies already delivered to another phone or stored on a Nostr relay. Local deletion and network deletion are different guarantees.

No account does not mean no metadata

BitChat avoids phone numbers and account records, but Bluetooth proximity is observable. A nearby radio observer can still learn that devices were present and exchanging traffic. Ephemeral peer IDs, padded packets, and daily-rotating courier tags reduce long-term correlation. They do not make radio activity invisible.

Human identity is another layer. A Noise handshake proves that the same cryptographic key is on the other end. It does not prove the nickname belongs to the person you think it does. BitChat uses fingerprints and optional in-person QR verification for that binding.

Radicle protects the code, not the chat

The BitChat mirror on Radicle solves a separate problem. BLE and Nostr move messages after the app is installed. Radicle replicates the Git repository so one code host cannot erase every source copy. The repository gets a stable ID derived from its identity document, and peers replicate signed Git references under their own namespaces.

Source availability does not install a safe binary on a phone. A mirror can lag, carry a different delegate, or be wrapped into a malicious APK. The Radicle mirror reported 66 seeders when we checked, but its main branch was behind GitHub. Users still need a trusted signing key, reproducible build process, and a way to learn which release is current.

Where the design fails

  • Range depends on people. Seven allowed hops mean nothing if there is no continuous chain of nearby devices.
  • Mobile operating systems can throttle scanning, advertising, and background work. Lock-screen behavior must be tested on real devices.
  • Bluetooth can be jammed, disabled, or watched. The mesh removes a cellular dependency, not every physical dependency.
  • Flood limits reduce spam and battery drain but do not remove Sybil or denial-of-service risk from many hostile peers.
  • Nostr fallback needs internet access, and relay availability and retention sit outside BitChat's control.
  • Courier delivery is eventual and probabilistic. A courier may never meet the recipient, may discard mail, or may run out of retention time.
  • The Android repository warns that it has not received an external security review. Protocol claims are not a substitute for an audit.

How to test a system like this

A simulator will miss the failures that matter. Put at least eight physical phones in a line, two disconnected groups, and one moving courier between them. Disable internet access. Lock screens, kill apps, remove a relay, rotate peers, and send duplicate and oversized packets. Measure delivery rate, latency, battery drain, queue expiry, and what survives a restart.

Then test trust. Compare fingerprints in person, install only a signed build, attempt a panic wipe, capture Bluetooth metadata, and compromise a courier without giving it the recipient's key. Record which messages arrive, how long they take, what observers learn, and exactly which failure stops delivery.

EngineeringBluetooth Low EnergyPeer-to-peerCryptographyBitChatBluetoothLENoiseProtocolNostrPeerToPeerMeshNetworkingOfflineFirstDistributedSystemsMobileEngineeringCensorshipResistance

Ready to know?

Send what you want checked or built. Fixed scope, price, and date in writing inside 24 hours, or the website or audit fee on your first project is refunded in full.

24 clock hours. Weekends included.