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

> Explore how BitChat discovers peers via Bluetooth LE on iOS. Learn about its layered architecture, fixed service UUID, scanning techniques, and intelligent connection scheduler.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-07-31

---

**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.

```swift
// 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:

```swift
// 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.

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/BLERadioController.swift) |
| **Scheduler** | Decides which discovered peripherals become connection candidates based on RSSI and budget | [`BLEConnectionScheduler.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEConnectionScheduler.swift) |
| **Engine** | Maintains mesh state, peer registry, and processes announce packets | [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift), [`BLEPeerRegistryStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEPeerRegistryStore.swift) |
| **App Boundary** | Exposes `Transport` API that other modules query for peers | [`Transport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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.