How BitChat's BLE Mesh Network Enables Offline Communication

BitChat implements a Bluetooth Low Energy mesh network that functions as a store-and-forward packet radio system, allowing messages to be stored locally and delivered when recipients reconnect.

BitChat, an open-source messaging application developed by permissionlesstech, leverages a custom BLE mesh network to enable communication without internet connectivity. This article explains how the mesh architecture processes, routes, and stores messages for offline delivery using the Swift implementation found in the permissionlesstech/bitchat repository.

BitChat BLE Mesh Architecture

The BitChat BLE mesh network is organized into a four-layer architecture that separates concerns between radio hardware, protocol logic, feature modules, and the application interface.

Four-Layer Stack

  • BLE Link Layer: Handles direct CoreBluetooth interaction including scanning, advertising, connection scheduling, and MTU management. Key implementations include BLELinkStateStore and BLERadioController in bitchat/Services/BLE/BLERadioController.swift.

  • Mesh Engine: Contains the serial-queue core that owns all protocol state including packet codecs, fragmentation, deduplication, and relay policies. This layer implements the primary handle(event) -> [Effect] loop in BLEService located at bitchat/Services/BLE/BLEService.swift, utilizing helpers like BLEFragmentAssemblyBuffer and BLEMeshPingTracker.

  • Feature Modules: Encapsulate specific capabilities such as courier services, message boards, and file transfer. Each module owns its state independently and registers for specific packet types without accessing engine internals. Examples include CourierStore, BoardStore, and PrekeyBundleStore.

  • App Boundary: Provides a thin Transport façade that the UI consumes through Transport protocol extensions on BLEService. This layer handles capability discovery and forwards high-level events to view models while abstracting mesh complexity.

Store-and-Forward Flow for Offline Communication

When a recipient device is unreachable, BitChat's mesh engine queues messages in the CourierStore, creating a store-and-forward system that functions similarly to packet radio networks.

Message Transmission Pipeline

  1. Message Creation: The UI invokes sendMessage(to:payload:ttl:completion:) on the Transport protocol, which is implemented by BLEService.

  2. Fragmentation: Payloads exceeding the BLE MTU are split by BLEFragmentAssemblyBuffer into fragments queued in BLEOutboundFragmentTransferScheduler.

  3. Routing Decision: The engine selects a next-hop based on MeshTopologyTracker and gossip policy thresholds. If the destination peer is unreachable, the packet routes to CourierStore for offline storage.

  4. Encryption: Each fragment receives Noise protocol encryption via NoiseSessionManager, binding the fragment to the specific physical link that authenticated it through BLELinkAuthState and BLELinkBindings.

  5. Transmission: BLELinkLayer writes bytes to BLE peripherals or centrals, respecting back-pressure buffers including pendingPeripheralWrites and pendingNotifications.

  6. Reception: Remote devices receive fragments through the link layer, which hands raw bytes to the engine's ingestDecodedPacket method for reassembly and Noise tag validation.

  7. Delivery or Storage: If the destination is online, the engine forwards the complete packet to feature modules via BitchatDelegate. If offline, the envelope persists in CourierStore—a lock-backed, main-actor cache that respects per-peer quotas defined by CourierDepositTier until the recipient reconnects.

Key Source Files and Components

Understanding the implementation requires familiarity with these specific files in the repository:

Practical Implementation Examples

Sending an Offline-Capable Message

To send a message that will be stored for offline delivery if necessary:

// Transport is a BLEService instance conforming to the Transport protocol
let transport: Transport = bleService
let payload = "Hello offline world!".data(using: .utf8)!

transport.sendMessage(
    to: .broadcast,
    payload: payload,
    ttl: TransportConfig.messageTTLDefault
) { result in
    switch result {
    case .success:
        print("Queued for mesh delivery or offline storage")
    case .failure(let error):
        print("Transmission failed: \(error)")
    }
}

This call traverses BLEService into BLEOutboundFragmentTransferScheduler, then BLELinkLayer. If no peers accept the packet, the engine stores it in CourierStore as implemented in BLEService around lines 258-260.

Receiving Messages via Delegate

Implement BitchatDelegate to handle incoming messages after the mesh engine verifies and reassembles them:

class ChatViewModel: BitchatDelegate {
    func didReceivePublicMessage(_ packet: BitchatPacket) {
        // Update UI with verified payload
        displayMessage(packet.payload)
    }
}

// Assign delegate to receive events exclusively through this interface
bleService.delegate = self

The delegate receives packets only after the engine successfully validates Noise tags and reassembles fragments via BLEFragmentAssemblyBuffer.

Accessing Offline Storage

When a peer reconnects, retrieve and attempt delivery of stored messages:

func handlePeerConnected(_ peerID: PeerID) {
    let pendingEnvelopes = CourierStore.shared.envelopesForPeer(peerID)
    
    pendingEnvelopes.forEach { envelope in
        bleService.tryDeliver(envelope)
    }
    
    CourierStore.shared.removeDelivered(envelopes: pendingEnvelopes)
}

CourierStore enforces CourierDepositTier quotas and provides lock-backed access suitable for main-actor consumption without blocking the mesh engine.

Concurrency and Safety Architecture

The mesh engine guarantees thread safety through strict queue separation. The engine queue operates serially, ensuring all state mutations—including peer registry updates, routing table changes, and Noise session management—occur without locks, preventing deadlocks. The link-layer queue handles only I/O operations and never touches engine state, maintaining a one-way synchronization edge from main to engine to bleQueue as documented in BLE-ARCHITECTURE-V3.md.

Deterministic Simulation and Testing

BitChat validates its offline logic through SimulatedLinkLayer, allowing the mesh engine to run off-device. The test suite in bitchatTests/Simulation/SimulatedMesh.swift drives the identical code paths used in production without real radios, proving that store-and-forward logic functions correctly even when devices remain out of range for extended periods.

Summary

  • BitChat implements a four-layer BLE mesh architecture separating radio hardware from protocol logic and application interfaces.
  • Offline communication works via CourierStore, a persistent cache that retains messages when recipients are unreachable and delivers them upon reconnection.
  • Fragmentation and encryption occur at the mesh engine layer using BLEFragmentAssemblyBuffer and NoiseSessionManager, ensuring data integrity across multiple hops.
  • Strict concurrency controls use serial queues to eliminate deadlocks while maintaining high-throughput packet processing.
  • Comprehensive simulation via SimulatedMesh validates offline behavior without hardware dependencies.

Frequently Asked Questions

How does BitChat handle message delivery when the recipient is completely out of range?

When the recipient is unreachable, the mesh engine stores the message in CourierStore, a lock-backed cache that persists envelopes until the destination peer reconnects. The store enforces per-peer quotas through CourierDepositTier to prevent resource exhaustion, ensuring messages survive temporary network partitions.

What encryption does BitChat use for mesh messages?

BitChat implements the Noise protocol through NoiseSessionManager, binding each packet fragment to the specific authenticated link that transmitted it. This link-bound encryption in BLELinkAuthState ensures fragments are only accepted on the physical link that originally authenticated them, preventing interception on alternate paths.

Can BitChat's mesh network simulate offline scenarios for testing?

Yes, the codebase includes SimulatedLinkLayer and SimulatedMesh in the test suite, which execute the identical mesh engine logic without hardware. These simulations validate store-and-forward behavior, fragmentation, and routing decisions in deterministic environments where devices are programmatically taken offline and brought back online.

What is the maximum message size supported by the BLE mesh?

The mesh handles payloads exceeding standard BLE MTU through BLEFragmentAssemblyBuffer, which automatically fragments large messages and reassembles them at the destination. While specific size limits depend on available CourierStore quota and memory constraints, the fragmentation layer abstracts MTU limitations from the application layer.

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 →