How BitChat Enables Offline Communication Using Bluetooth Low Energy (BLE) Mesh
BitChat implements a dual-transport architecture that pairs the global Nostr protocol with a local Bluetooth Low Energy mesh, allowing encrypted messages to route through a multi-hop peer-to-peer network when internet connectivity is unavailable.
BitChat (permissionlesstech/bitchat) is an open-source messaging application designed to function without centralized infrastructure. By leveraging a Bluetooth Low Energy (BLE) mesh transport layer, the application establishes direct device-to-device communication that operates entirely offline, falling back to Nostr only when no mesh peers are available.
Dual-Transport Architecture Overview
The application operates on a dual-transport principle that prioritizes local connectivity. When internet access is present, BitChat can utilize the Nostr protocol for global message propagation. However, when connectivity drops or users choose to remain offline, the system seamlessly transitions to the BLE mesh transport. This mesh functions as a multi-hop, packet-radio stack built on CoreBluetooth, enabling devices to discover each other, form ad-hoc networks, and relay encrypted packets up to seven hops away according to the mesh topology constraints defined in README.md.
Layered BLE Mesh Stack
BitChat’s Bluetooth implementation follows a strict layered architecture defined in docs/BLE-ARCHITECTURE-V3.md, separating concerns to ensure deterministic behavior on resource-constrained devices.
BLELinkLayer (CoreBluetooth Interface)
The BLELinkLayer serves as the sole interface to CoreBluetooth, handling all direct radio operations including scanning, advertising, connection scheduling, MTU handling, and back-pressure buffers. This layer exposes only LinkEvent and LinkCommand abstractions to higher levels, remaining completely agnostic to packet contents or encryption sessions. By isolating CoreBluetooth imports to this layer alone, the architecture prevents cross-cutting dependencies and simplifies testing.
Mesh Engine (Protocol State Management)
The Mesh Engine operates on a dedicated serial queue that owns all protocol state, including fragmentation, deduplication, relay policy, peer registry, topology management, gossip sync, and Noise orchestration. Running all mesh logic on a single-writer queue guarantees deterministic ordering and eliminates deadlock scenarios during complex routing decisions. This engine implements the fan-out selector that determines which peers receive specific packets, optimizing broadcast scope to conserve battery life.
Feature Modules and App Boundary
Individual capabilities such as private media sharing, voice messages, and courier services each maintain their own state as Feature Modules, registering for specific message types without modifying the core engine. The BLEService class acts as the App Boundary, implementing the Transport protocol that the UI layer consumes. Located in bitchat/Services/BLE/BLEService.swift, this façade forwards events to the UI via BitchatDelegate and delegates outbound messages to the underlying mesh layers.
Offline Message Routing Flow
When operating without internet connectivity, BitChat follows a deterministic sequence for message propagation through the BLE mesh.
Discovery and Mesh Formation
Each device simultaneously operates as both a Central and Peripheral, advertising a common service UUID (F47B5E2D-...). The BLELinkLayer continuously scans for nearby peers and establishes connections automatically. Once connected, devices exchange capability information and populate the peer registry maintained by the Mesh Engine.
Multi-Hop Relay and Fan-Out
Outbound messages undergo encoding into a compact binary protocol optimized for BLE’s limited MTU of approximately 512 bytes. If messages exceed the default fragment size, the Mesh Engine handles fragmentation transparently. The engine serializes packets and passes raw bytes to the link layer via LinkCommand instructions. For delivery, the fan-out selector determines the optimal next-hop neighbors, limiting unnecessary broadcasts while ensuring packets progress toward their destinations up to the configured hop limit.
Store-and-Forward Queuing
If a recipient is temporarily out of range, the Mesh Engine implements smart-queuing logic described in README.md (lines 89-102). The engine stores undelivered messages locally in a persistent queue. When the target peer reconnects directly or appears through an intermediate node, the queued message automatically transmits without user intervention. This store-and-forward mechanism ensures message persistence across intermittent connectivity.
Security and Reliability Mechanisms
Noise Protocol Encryption
All traffic traversing the BLE mesh undergoes encryption using the Noise Protocol, providing forward secrecy for active sessions and end-to-end confidentiality for stored messages. The Mesh Engine orchestrates Noise handshake and decryption routines, ensuring that packet contents remain encrypted during multi-hop transit and are only decrypted by the final recipient.
Power Management and TTL
To prevent battery drain and network congestion, the mesh enforces a Time-To-Live (TTL) counter on each packet, defaulting to 5 hops (with a maximum of 7). When a packet’s TTL expires, nodes discard it automatically. Additionally, the BLELinkLayer maintains back-pressure buffers to prevent overwhelming the radio, while the Mesh Engine employs adaptive power logic that throttles scanning and advertising frequencies based on device battery state. The BLEPeerRegistryStore provides lock-backed peer state for fast, non-blocking reads essential for low-latency routing.
Implementation Code Examples
Initialize the BLE mesh service and configure the delegate:
import bitchat
// Initialize the singleton BLE service
let bleService = BLEService()
// Configure the delegate to receive messages
class ChatViewModel: BitchatDelegate {
func didReceivePublicMessage(_ msg: BitchatMessage) {
// Handle message received via BLE mesh
print("Received: \(msg.payload)")
}
}
let viewModel = ChatViewModel()
bleService.delegate = viewModel
Send a text message with TTL constraints:
// Create a message with 5-hop maximum
let message = BitchatMessage(
type: .text,
payload: "Offline mesh test".data(using: .utf8)!,
ttl: 5 // Expires after 5 hops
)
// Send through the mesh (encrypts with Noise, fragments if needed)
bleService.sendMessage(message)
The sendMessage method in bitchat/Services/BLE/BLEService.swift (approximately lines 200-210) internally routes the payload through the Mesh Engine, which handles Noise encryption, packet fragmentation, and fan-out selection before handing raw bytes to the BLELinkLayer for transmission.
Handle queueing when no peers are available:
// This returns immediately even if no peers are connected
bleService.sendMessage(urgentMessage)
// The Mesh Engine automatically queues the message.
// When a peer appears (directly or via relay),
// transmission occurs without additional code.
Summary
- BitChat combines Nostr and BLE mesh in a dual-transport architecture that prioritizes offline peer-to-peer communication.
- The Mesh Engine manages routing, encryption, and queuing on a serial queue, while the BLELinkLayer handles all CoreBluetooth interactions.
- Messages can travel up to 7 hops through the mesh using a fan-out selector that optimizes for battery efficiency.
- Noise Protocol encryption ensures end-to-end security across multiple relay nodes.
- Store-and-forward queuing automatically handles intermittent connectivity, delivering messages when peers become available.
Frequently Asked Questions
How many hops can a message travel in the BitChat BLE mesh?
Messages can traverse up to seven hops through the mesh network, though the default Time-To-Live (TTL) is set to 5 hops to balance delivery reliability with battery conservation. Each intermediate node decrements the TTL before relaying; when TTL reaches zero, the packet drops automatically to prevent infinite circulation.
What encryption protocol does BitChat use for BLE mesh messages?
BitChat encrypts all BLE mesh traffic using the Noise Protocol, which provides forward secrecy for active sessions. According to the architecture documentation in docs/BLE-ARCHITECTURE-V3.md, the Mesh Engine orchestrates Noise handshake routines, ensuring that only the intended recipient can decrypt message contents even when packets traverse multiple intermediate nodes.
How does BitChat handle messages when the recipient is out of Bluetooth range?
When a recipient is unreachable, the Mesh Engine activates its smart-queuing mechanism (detailed in README.md lines 89-102). The message persists in local storage until the recipient comes within direct range or appears through a newly connected intermediate node. At that point, the engine automatically retransmits the queued message without requiring user action, effectively providing store-and-forward capability across the mesh.
Does BitChat require internet to form a BLE mesh?
No. The BLE mesh operates completely offline using CoreBluetooth to form ad-hoc networks between nearby devices. The mesh forms automatically when devices running BitChat detect each other’s advertised service UUID (F47B5E2D-...). Internet connectivity is only required when falling back to the Nostr protocol for global message propagation; local mesh communication requires only Bluetooth Low Energy hardware.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →