How the BLE Link Layer in BitChat Interacts with CoreBluetooth: Architecture Guide
BitChat's BLEService class implements both CBCentralManagerDelegate and CBPeripheralManagerDelegate to simultaneously act as central and peripheral, marshaling every raw CoreBluetooth event into a thread-safe link-state model and forwarding it to the app via TransportEvent.
BitChat is an open-source peer-to-peer chat application that implements a custom Bluetooth Low Energy link layer directly on top of Apple's CoreBluetooth framework. Understanding how the BLE link layer in BitChat interacts with CoreBluetooth is essential for developers building offline-first mesh messaging apps on iOS. This guide breaks down the architecture, thread-safety model, and event flow using the actual source from the permissionlesstech/bitchat repository.
Architecture Overview
The BLE stack in BitChat is organized into three logical layers that sit between CoreBluetooth and the app's UI.
- Core-Bluetooth Bridge: Encapsulated in
BLEService, this layer ownsCBCentralManagerandCBPeripheralManagerand receives all raw delegate callbacks such asdidDiscover,didConnect, anddidUpdateValue. - Link-State Store: Normalizes each physical connection into a
BLELinkobject tracked byBLELinkStateStore,BLELinkAuthState, andBLELinkBindings. All mutations occur on the dedicated serialbleQueue. - Transport-Level Interface: Exposes high-level events including
TransportEvent.bluetoothStateUpdatedand peer-snapshot changes to consumers likeChatViewModelthrough theTransportprotocol.
CoreBluetooth to BLE Link Layer Flow
Initialization and Queue Setup
In bitchat/Services/BLE/BLEService.swift, the initializer creates a dedicated serial dispatch queue named bleQueue. When initializeBluetoothManagers is true, it instantiates both CBCentralManager and CBPeripheralManager on that queue (see lines 26-34). Running CoreBluetooth on a private queue prevents delegate callbacks from stalling the main thread.
Scanning and Advertising
The BLERadioController object drives radio behavior. It calls centralManager?.scanForPeripherals to discover peers and peripheralManager?.startAdvertising to announce the local device. Scanning results arrive through the delegate method centralManager(_:didDiscover:advertisementData:rssi:).
Connection Lifecycle
When a remote peripheral is discovered, BLEService initiates a connection via centralManager?.connect. Successful connections trigger centralManager(_:didConnect:), while disconnections or errors trigger centralManager(_:didDisconnectPeripheral:error:). Each event updates the internal connection graph maintained by the link-state store.
Data Transfer and Fragmentation
Peripheral write requests enter the link layer through peripheralManager(_:didReceiveWrite:). The service buffers incoming CBATTRequest objects in BLEInboundWriteBuffer and eventually hands them to BLEFragmentHandler for reassembly. Because BLE MTU is limited, outbound packets are split by BLEOutboundFragmentTransferScheduler and written using CBPeripheralManager.updateValue or CBPeripheral.writeValue.
Bluetooth State Propagation
All CBManagerState changes, including centralManagerDidUpdateState and peripheralManagerDidUpdateState, are converted into TransportEvent.bluetoothStateUpdated(state) (see lines 82-84 of bitchat/Services/Transport.swift). Downstream observers use this event to react to powered-on, powered-off, or unauthorized states without touching CoreBluetooth directly.
Thread Safety and the Link-State Store
BLELinkStateStore and Queue Isolation
BLELinkStateStore is the single source of truth for physical link state. It is accessed exclusively from the serial bleQueue, and synchronous reads are performed inside a bleQueue.sync block via readLinkState. This design guarantees that BLELink identity, connection status, and advertising state never race against UI threads or the engine's messageQueue.
Authentication and Binding Security
BLELinkAuthState tracks whether a link has been signature-verified, gating encrypted payload acceptance. BLELinkBindings ties an active Noise session to a specific physical BLELink, preventing replay attacks where a forged announce could bind an absent peer ID to a stale connection. According to source comments in BLEService.swift (lines 22-31), both structures follow an engine-owned contract—mutated only on the serial messageQueue and read by the link layer under bleQueue protection—that enforces the option-B isolation model.
Transport Event Propagation to the UI
BLEService does not expose CoreBluetooth objects to ChatViewModel or other services. Instead, it translates every significant event into a TransportEvent value. The Transport protocol defines eventDelegate of type TransportEventDelegate, which receives method calls such as didReceiveTransportEvent(_:).
When Bluetooth is disabled, the emitted .bluetoothStateUpdated(let state) event allows the view model to toggle a SwiftUI alert by checking state != .poweredOn. This indirection keeps UI code free of CoreBluetooth imports.
Practical Code Examples
Initializing the BLE Service
let bleService = BLEService(
keychain: KeychainManager.shared,
idBridge: NostrIdentityBridge(),
identityManager: SecureIdentityStateManager.shared,
initializeBluetoothManagers: true
)
bleService.startServices()
Calling startServices() schedules an initial announce after a short delay (see lines 65-73 of bitchat/Services/BLE/BLEService.swift).
Sending a Public Broadcast Message
bleService.sendMessage(
"Hello, mesh!",
mentions: [],
to: nil,
messageID: UUID().uuidString,
timestamp: Date()
)
This builds a BitchatPacket, signs it with the Noise service, records a deduplication ID, and calls broadcastPacket(_:) to write the data to every connected link (see lines 86-92 of bitchat/Services/BLE/BLEService.swift).
Handling an Incoming Peripheral Write
func peripheralManager(_ peripheral: CBPeripheralManager,
didReceiveWrite requests: [CBATTRequest]) {
pendingWriteBuffers.append(contentsOf: requests)
}
The delegate method—implemented later in BLEService.swift—stores raw bytes in BLEInboundWriteBuffer. Later, BLEFragmentHandler running on the messageQueue reassembles complete packets and emits TransportEvent.messageReceived.
Observing Bluetooth State in a SwiftUI View Model
class ChatViewModel: ObservableObject {
@Published var showBluetoothAlert = false
init(transport: Transport) {
transport.eventDelegate = self
}
}
extension ChatViewModel: TransportEventDelegate {
func didReceiveTransportEvent(_ event: TransportEvent) {
if case .bluetoothStateUpdated(let state) = event {
showBluetoothAlert = (state != .poweredOn)
}
}
}
ChatViewModel receives state updates through receiveTransportEvent(_:) in bitchat/Services/Transport.swift and updates the UI accordingly.
Key Source Files
bitchat/Services/BLE/BLEService.swift— Core BLE implementation, central and peripheral managers, and message processing.bitchat/Services/Transport.swift—Transport,TransportEvent, and the bridge that forwards BLE events to higher-level app layers.bitchat/Services/BLE/LinkState/BLELinkStateStore.swift— Normalizes physical connections intoBLELinkobjects and tracks connection state.bitchat/Services/BLE/Auth/BLELinkAuthState.swift— Holds per-link authentication flags such as signature-verified status.bitchat/Services/BLE/Bindings/BLELinkBindings.swift— Maps Noise sessions to physical links to prevent replay attacks.bitchat/Services/BLE/Fragmentation/BLEFragmentHandler.swift— Reassembles packets that exceed the BLE MTU.bitchat/Services/BLE/IO/BLEIncomingWriteBuffer.swift— Buffers incomingCBATTRequestobjects until fragments are complete.bitchat/Services/BLE/Radio/BLERadioController.swift— Drives scanning, advertising, duty-cycle management, and exposes the link-state store to the engine.
Summary
BLEServicewraps CoreBluetooth by implementing bothCBCentralManagerDelegateandCBPeripheralManagerDelegateinside a single service class.- A dedicated serial
bleQueueisolates CoreBluetooth callbacks and link-state mutations from the UI and engine queues. BLELinkStateStore,BLELinkAuthState, andBLELinkBindingsprovide a normalized, thread-safe model of every physical link.- Large messages are transparently fragmented and reassembled using
BLEFragmentHandlerandBLEInboundWriteBuffer. - The rest of the application consumes BLE changes exclusively through
TransportEvent, decoupling the chat logic from CoreBluetooth internals.
Frequently Asked Questions
What is the role of BLEService in BitChat's CoreBluetooth stack?
BLEService is the sole bridge between CoreBluetooth and the application. It instantiates CBCentralManager and CBPeripheralManager on a private bleQueue, handles every delegate callback, and translates raw Bluetooth events into structured link-state updates that the engine and UI can consume safely.
How does BitChat ensure thread safety between CoreBluetooth and the UI?
All CoreBluetooth initialization and BLELinkStateStore mutations occur on a serial dispatch queue called bleQueue. The UI and engine read link state through bleQueue.sync blocks, and higher-level code receives asynchronous TransportEvent updates. This prevents race conditions without blocking the main thread.
What happens when a BLE message exceeds the standard MTU?
BitChat fragments large packets using BLEFragmentHandler. Incoming bytes from peripheralManager(_:didReceiveWrite:) are buffered in BLEInboundWriteBuffer until the full fragment set arrives. Outbound data is scheduled by BLEOutboundFragmentTransferScheduler and transmitted in chunks via CBPeripheral.writeValue or CBPeripheralManager.updateValue.
How does the app detect when Bluetooth is powered off or unavailable?
State changes in CoreBluetooth trigger centralManagerDidUpdateState or peripheralManagerDidUpdateState inside BLEService. The service immediately emits TransportEvent.bluetoothStateUpdated(state) (see bitchat/Services/Transport.swift). ChatViewModel observes this event and presents a SwiftUI alert whenever the state is not .poweredOn.
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 →