How Message Routing Works in BitChat: A Deep Dive Into Its Three-Layer Architecture

BitChat routes messages through three overlapping layers—transport selection, BLE mesh source-routing, and courier fallback—to ensure reliable, privacy-preserving delivery even when internet connectivity fails.

Understanding message routing in BitChat requires examining how the open-source Swift application balances immediacy against resilience. The permissionlesstech/bitchat repository implements a sophisticated router that automatically selects between direct transports, computes multi-hop Bluetooth Low Energy (BLE) routes, and falls back to sealed courier deposits when no path exists. This architecture enables offline-first messaging that survives network partitions while keeping content encrypted end-to-end.

Peer Identification and Routing Data

Every peer in BitChat is identified by a routing ID—an 8-byte value derived from either a short Nostr-DM identifier or a Noise public key. This compact identifier powers all mesh-level forwarding decisions.

The conversion happens in bitchat/Nostr/PeerID.swift at line 91:

let peerID = PeerID(str: "nostr:deadbeef")
let routingData: Data = peerID.routingData  // 8-byte mesh routing identifier

The routingData property strips away cryptographic material, exposing only the minimum information the BLE mesh requires to forward packets. This design choice limits metadata exposure on the wireless medium while enabling efficient routing table lookups.

Transport Selection and Priority Logic

The MessageRouter class in bitchat/Services/MessageRouter.swift implements a four-tier priority system for transport selection. At line 92, sendPrivate(_:to:recipientNickname:messageID:) evaluates available transports in strict precedence:

  1. Connected & secure — A transport with an active, end-to-end encrypted session to the target peer
  2. Connected but insecure — A transport with connectivity but without cryptographic authentication
  3. Reachable — A transport reporting the peer as reachable (typically via BLE mesh observation)
  4. Courier fallback — Depositing sealed copies on trusted intermediary peers or Tor bridges
let router = MessageRouter(transports: [bleTransport, nostrTransport])
router.sendPrivate(
    "Hello, world!",
    to: PeerID(str: "nostr:deadbeef"),
    recipientNickname: "Alice",
    messageID: UUID().uuidString
)

When a secure session exists (canDeliverSecurely), the router immediately enqueues the message and flags it as a secure transmission. If only the mesh reports reachability, the router defers to BLE source-routing. Otherwise, it triggers courier logic.

BLE Mesh Source-Routing

BitChat implements source routing for its BLE mesh layer, meaning the sender pre-computes the full path rather than relying on per-hop forwarding tables. This approach suits resource-constrained mobile devices operating in intermittent connectivity environments.

At bitchat/Services/BLE/BLEService.swift line 1001, computeRoute(to:) queries the local topology:

let route = bleService.computeRoute(to: peerID)  // -> [Data]?

The returned array contains routing IDs for each intermediate hop. If a valid route exists, line 1124's applyRouteIfAvailable(_:to:) performs three critical operations:

  1. Injects the computed route into the BitchatPacket headers
  2. Upgrades the packet to protocol version 2
  3. Re-signs the packet with the sender's private key
var packet = BitchatPacket(...)
packet = bleService.applyRouteIfAvailable(packet, to: peerID)

Protocol version 2 distinguishes routed packets from direct transmissions and includes hop-limit fields to prevent indefinite forwarding loops. The re-signing step ensures route injection cannot be performed by attackers without the sender's keys.

Courier Fallback and Bridge Deposits

When no transport can deliver promptly, BitChat's courier system provides asynchronous reliability. The router maintains an outbox of pending messages and attempts multiple deposit strategies:

  • Bridge deposits — Posting sealed envelopes to Nostr relays when bridge mode is enabled
  • Peer couriers — Selecting up to three trusted peers (mutual favorites) via eligibleCouriers
  • Tor bridge integration — Depositing on Tor hidden services when configured

At line 101, courierBecameAvailable(_:) enables opportunistic retry:

func meshDidConnect(_ peerID: PeerID) {
    router.courierBecameAvailable(peerID)
}

When a new peer connects to the mesh, the router re-evaluates pending courier deposits. This handles scenarios where a courier was offline when the message originated but later rejoins the network.

The courier system enforces resource limits through maxMessagesPerPeer and maxCouriersPerMessage, preventing spam amplification and memory exhaustion attacks.

Delivery Lifecycle and Acknowledgments

Message routing concludes with explicit delivery confirmation. When a recipient generates a delivery or read acknowledgment, the router invokes clearRetainedMessage to remove the pending copy from its outbox. This state transition updates the UI, removing the "sending" indicator and confirming successful transit through whatever path the router ultimately utilized.

Summary

  • Peer identification uses 8-byte routing data derived from Nostr or Noise keys via PeerID.routingData in bitchat/Nostr/PeerID.swift
  • Transport selection follows a strict priority: connected secure → connected insecure → reachable → courier, implemented in MessageRouter.sendPrivate
  • BLE source-routing pre-computes paths through BLEService.computeRoute(to:) and applies them with applyRouteIfAvailable, upgrading to protocol version 2
  • Courier fallback deposits sealed envelopes on trusted peers and bridges, with opportunistic retry when couriers become available
  • Resource limits and re-signing protect against amplification attacks and route tampering

Frequently Asked Questions

What is the 8-byte routing ID in BitChat?

The 8-byte routing ID is a compressed peer identifier derived from either a Nostr public key or a Noise public key. Implemented in PeerID.routingData at bitchat/Nostr/PeerID.swift line 91, it provides the minimal information needed for BLE mesh forwarding without exposing full cryptographic identities to the wireless medium.

How does BitChat route messages when the internet is unavailable?

BitChat falls back to BLE mesh source-routing. The MessageRouter checks if the target peer is reachable via mesh observation, then BLEService.computeRoute(to:) calculates a multi-hop path through observed peers. If no route exists, the message enters the courier outbox for eventual delivery when connectivity returns.

What is the courier system in BitChat?

The courier system provides asynchronous message delivery by depositing sealed, encrypted envelopes on trusted intermediary peers or Tor bridges. Located in MessageRouter lines 92-101, it selects up to three mutual-favorite peers and retries deposits when new couriers connect, ensuring messages survive extended network partitions.

Why does BitChat use source routing instead of hop-by-hop forwarding?

Source routing pre-computes the entire path at the sender, reducing per-hop state on resource-constrained mobile devices. This design matches BitChat's BLE mesh environment where topology changes frequently and intermediate nodes lack persistent storage or stable power. The applyRouteIfAvailable method in BLEService.swift implements this by embedding the route directly into packet headers.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →