How BitChat Discovers Peers Using Bluetooth LE: A Deep Dive into the iOS Implementation

BitChat discovers nearby peers using a layered Bluetooth LE architecture that separates radio-level scanning from higher-level mesh logic, advertising a fixed service UUID, scanning with allow-duplicates enabled, and using a connection scheduler to intelligently decide which peripherals to connect based on RSSI, recent traffic, and connection budgets.

BitChat is an open-source, peer-to-peer messaging app developed by permissionlesstech that operates without centralized servers. To discover peers using Bluetooth LE, the app implements a sophisticated transport layer that isolates radio management from peer-selection policies. This design ensures continuous discovery while preventing UI blocking or deadlocks in the mesh network.

The Bluetooth LE Discovery Flow

BitChat’s discovery mechanism follows an eight-step process that moves from low-level radio scanning to high-level mesh state management. Each step is designed to minimize power consumption while maximizing the chances of establishing usable peer connections.

Advertising the BitChat Service UUID

Every BitChat node begins by advertising its presence using a fixed service UUID defined in BLEService.serviceUUID. This advertisement data is constructed in BLEService.swift within the BLEService.advertisementData property. By broadcasting this specific UUID, BitChat nodes can immediately identify compatible peers without wasting resources on irrelevant Bluetooth devices.

Scanning for Peripherals

The central-role radio implementation in BLERadioController handles active scanning. According to the source code in bitchat/Services/BLE/BLERadioController.swift (lines 73-86), the app starts scanning with the allow-duplicates option enabled while the app is active. This configuration yields immediate discovery events instead of coalesced updates, ensuring the app detects nearby peers as soon as they come within range.

// Initiated via BLERadioController.startScanning()
func startScanning() {
    // Configured with CBCentralManagerScanOptionAllowDuplicatesKey: true
    central.scanForPeripherals(withServices: [BLEService.serviceUUID], 
                               options: scanOptions)
}

Discovery Callback and Scheduling

When CoreBluetooth discovers a peripheral, the system invokes the delegate method implemented in bitchat/Services/BLE/BLERadioController.swift (lines 55-100). The handleDiscovery method creates a BLEConnectionCandidate containing the peripheral instance, RSSI value, and timestamp, then passes this candidate to the BLEConnectionScheduler.

The scheduler acts as a gatekeeper, applying peer-selection policies that isolate connection logic from radio code. Based on signal strength, recent traffic patterns, and current connection budgets, the scheduler returns one of five decisions: ignore, queue, retry later, cancel a stale connection, or connect immediately.

Connection and Service Discovery

If the scheduler returns .connectNow, BLERadioController.beginCentralConnection initiates the CoreBluetooth connection. After the peripheral connects, the code triggers service discovery specific to the BitChat protocol:

// Called after peripheral connects
func peripheral(_ peripheral: CBPeripheral, didConnect error: Error?) {
    guard error == nil else { return }
    // Discover the specific BitChat service
    peripheral.discoverServices([BLEService.serviceUUID])
}

This service discovery result drives updates to the BLEPeerRegistryStore, informing the mesh engine about the new peer's capabilities and availability.

Announcement Handshake

Once the BLE link is established, BLEAnnounceHandler sends an announce packet containing the peer’s public identity and capability bits. As documented in BLE-ARCHITECTURE-V3.md (lines 31-36), the remote side reciprocates with its own announce packet, establishing a bidirectional discovery path that completes the high-level peer introduction.

func sendAnnounce(to link: BLELink) {
    let packet = AnnouncePacket(identity: localIdentity, 
                                capabilities: localCapabilities)
    link.send(packet)  // Handled by BLEAnnounceHandler
}

Peer Registry Management

Discovered peer identities, capabilities, and link states are stored in a lock-backed BLEPeerRegistryStore. This registry maintains thread-safe access to peer state, allowing the transport layer to expose a consistent list of known peers to the rest of the application without race conditions.

Architecture Layers and Separation of Concerns

BitChat’s discovery system is organized into four distinct layers, each with specific responsibilities and dedicated source files:

Layer Responsibility Key File
Radio (BLELinkLayer) Scanning, advertising, low-level connection handling; no peer decisions BLERadioController.swift
Scheduler Decides which discovered peripherals become connection candidates based on RSSI and budget BLEConnectionScheduler.swift
Engine Maintains mesh state, peer registry, and processes announce packets BLEService.swift, BLEPeerRegistryStore.swift
App Boundary Exposes Transport API that other modules query for peers Transport.swift

This separation ensures that radio-level code in BLERadioController never makes policy decisions about which peers to prioritize, while the scheduler operates without knowledge of CoreBluetooth specifics.

Implementation Details: Key Files and Functions

The discovery flow relies on coordination between several critical components:

  • BLEService.swift: Creates the radio instance, manages the peer registry, and processes incoming announce packets to validate peer identities.
  • BLERadioController.swift: Implements central-role radio policy including scanning configuration, discovery handling via handleDiscovery, and connection initiation through beginCentralConnection.
  • BLEConnectionScheduler.swift: Contains the decision logic for converting discovery events into connection attempts, preventing connection storms by respecting RSSI thresholds and global connection limits.
  • BLERecentPeripheralCache.swift: Maintains a cache of recently seen peripherals to enable background wake-on-proximity connections without requiring constant scanning.
  • BLEAnnounceHandler.swift: Manages the final handshake that transforms a BLE connection into a BitChat peer relationship.

Summary

  • BitChat uses a fixed service UUID advertisement to identify compatible peers efficiently.
  • Allow-duplicates scanning provides immediate discovery events rather than batched updates.
  • The BLEConnectionScheduler decouples radio discovery from connection decisions, optimizing for RSSI, traffic, and connection budgets.
  • Service discovery and announce packets finalize the peer relationship after low-level BLE connection.
  • Lock-backed registries ensure thread-safe peer state management throughout the mesh.

Frequently Asked Questions

How does BitChat avoid connecting to every Bluetooth device nearby?

BitChat filters discovered peripherals using the BLEConnectionScheduler, which evaluates signal strength (RSSI), recent connection history, and current connection budgets before initiating a connection. Only peripherals advertising the specific BLEService.serviceUUID are considered candidates, and the scheduler may queue, ignore, or defer connections based on real-time conditions.

What is the purpose of the allow-duplicates scanning option?

According to the implementation in BLERadioController.swift, the allow-duplicates option (lines 73-86) ensures CoreBluetooth delivers discovery events immediately rather than coalescing them. This enables faster peer detection and more accurate RSSI tracking, which is critical for the scheduler’s connection decisions.

How does BitChat maintain peer information after discovery?

Once a peer is discovered and connected, BitChat stores the peer’s identity, capabilities, and connection state in the BLEPeerRegistryStore. This lock-backed registry provides thread-safe access to peer state and serves as the authoritative source for the rest of the application when querying available mesh participants.

Can BitChat discover peers when the app is in the background?

Yes, through the BLERecentPeripheralCache, BitChat remembers recently seen peripherals to enable background wake-on-proximity connections. Combined with Bluetooth LE’s ability to respond to advertisements while backgrounded, the app can maintain mesh connectivity without requiring constant foreground scanning.

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 →