When Does BitChat Switch Between Bluetooth Mesh and Nostr Transports?
BitChat automatically selects Bluetooth mesh when available and falls back to Nostr only when Bluetooth is unavailable, with smart queuing when neither transport is reachable.
BitChat is an open-source, peer-to-peer messaging app that seamlessly bridges local Bluetooth mesh networks with the global Nostr protocol. Understanding its transport-switching logic helps developers build resilient messaging features and troubleshoot connectivity issues. This article explains exactly when and how BitChat switches between its two transport layers, based on the actual source code in the permissionlesstech/bitchat repository.
How BitChat's Transport Selection Works
The transport decision happens at message-send time through an intelligent priority system. The ChatViewModel class in bitchat/ViewModels/ChatViewModel.swift orchestrates this process, monitoring Bluetooth state and routing each message to the optimal path.
The selection follows three strict rules:
| Priority | Condition | Transport Used |
|---|---|---|
| 1 | Bluetooth powered on and mesh link exists | Bluetooth mesh — direct, Noise-encrypted, lowest latency |
| 2 | Bluetooth unavailable (powered off, unauthorized, no peer) | Nostr fallback — relays via recipient's public key |
| 3 | Neither transport reachable | Smart queuing — local storage until connectivity returns |
This "Bluetooth First, Nostr Fallback, Smart Queue" policy is documented in the README at lines 87-95 and implemented throughout the core messaging stack.
The Core Switching Logic in ChatViewModel
The actual transport selection occurs in ChatViewModel.sendMessage(_:to:) and related methods. The view model maintains Bluetooth awareness through BluetoothStateReporting, updating its internal state via updateBluetoothState(_:) at line 1481.
Here's the simplified decision pattern from the source:
func sendMessage(_ payload: Data, to peer: Peer) async throws {
// 1️⃣ Attempt Bluetooth mesh first
if let mesh = meshService as? BluetoothStateReporting,
mesh.getCurrentBluetoothState() == .poweredOn,
let link = mesh.link(to: peer) {
try await mesh.send(payload, over: link) // Noise-encrypted mesh
return
}
// 2️⃣ Fallback to Nostr network
let nostrKey = peer.nostrPublicKey
let envelope = BitChatPrivateEnvelope.encrypt(payload, for: nostrKey)
try await nostrTransport.publish(envelope) // Relay-based delivery
}
The showBluetoothAlert flag tracks UI state, but the critical routing decision depends on getCurrentBluetoothState() and whether mesh.link(to:) returns a valid connection.
Bluetooth Mesh: The Preferred Transport
BitChat always attempts Bluetooth Low Energy mesh first when conditions allow. This path provides:
- Direct peer-to-peer delivery — no intermediate servers
- Noise protocol encryption — cryptographic session establishment
- Sub-100ms latency — locally routed packets
The BLE layer lives in bitchat/Transport/BLEService.swift, which manages CoreBluetooth state, mesh link formation, and encrypted packet exchange. A mesh link exists only when both devices have discovered each other through the Bluetooth mesh layer and completed the Noise handshake.
When Nostr Becomes the Fallback
Nostr activation triggers on any Bluetooth failure state:
poweredOff— Bluetooth disabled system-wideunauthorized— user denied Bluetooth permissionsresetting— Bluetooth hardware temporarily unavailableunsupported— device lacks Bluetooth capability- No reachable peer — recipient out of radio range or offline
In these cases, BitChatPrivateEnvelope.encrypt() wraps the payload for the recipient's Nostr public key, and NostrTransport.publish() relays it through configured Nostr relays in bitchat/Transport/NostrTransport.swift.
The Nostr path maintains privacy through app-specific envelope encryption in addition to Nostr's native encryption, ensuring BitChat messages remain unreadable to relay operators.
Smart Queuing for Offline Scenarios
When both transports fail — Bluetooth unavailable AND Nostr unreachable (no internet, relays down, or recipient has no Nostr identity) — BitChat does not drop the message.
Instead, the UnifiedPeerService in bitchat/Services/UnifiedPeerService.swift implements smart queuing: messages persist locally and automatically transmit once either transport becomes viable. This happens without user intervention; the app monitors state changes and drains the queue opportunistically.
Practical Code Examples
Checking Which Transport Would Be Used
Determine the active transport for a peer before sending:
import BitChat
func currentTransport(for peer: Peer) async -> TransportType {
if let mesh = viewModel.meshService as? BluetoothStateReporting,
mesh.getCurrentBluetoothState() == .poweredOn,
mesh.link(to: peer) != nil {
return .bluetoothMesh
} else {
return .nostr
}
}
Returns .bluetoothMesh for local links, .nostr otherwise.
Sending With Automatic Transport Selection
Let BitChat choose the optimal path:
let text = "Hello from the local mesh!"
let data = text.data(using: .utf8)!
do {
try await viewModel.sendMessage(data, to: targetPeer)
print("Message dispatched via the optimal transport.")
} catch {
print("Failed to send – will be queued until a transport becomes available.")
}
The sendMessage call transparently implements Bluetooth-first → Nostr-fallback → queue behavior.
Key Source Files
| File | Role |
|---|---|
bitchat/ViewModels/ChatViewModel.swift |
Central view-model; monitors Bluetooth state via updateBluetoothState(_:) and routes messages |
bitchat/Services/UnifiedPeerService.swift |
Unified abstraction over mesh and Nostr; implements fallback and queuing |
bitchat/Transport/NostrTransport.swift |
Nostr relay connections, private-envelope encryption, publishing |
bitchat/Transport/BLEService.swift |
CoreBluetooth management, mesh links, Noise-encrypted exchange |
README.md (lines 87-95) |
High-level transport policy documentation |
Summary
- Bluetooth mesh is always preferred when powered on and a peer link exists — fastest and most private path
- Nostr activates automatically when Bluetooth is unavailable (any failure state) or no mesh peer is reachable
- Smart queuing handles total offline scenarios, delivering messages when connectivity returns
- The switching logic lives in
ChatViewModel.swiftwithBluetoothStateReportingmonitoring andUnifiedPeerServiceabstraction - Developers interact with this system through simple
sendMessagecalls; transport selection happens transparently
Frequently Asked Questions
How does BitChat know if a Bluetooth mesh link exists?
BitChat checks mesh.link(to: peer) in the BLEService layer — this returns a valid link only when the mesh layer has discovered the peer and completed the Noise protocol handshake. The check happens fresh on every send attempt, so link state changes are detected in real-time.
Can I force a message to use Nostr even if Bluetooth is available?
The public API in ChatViewModel follows the automatic selection policy. To override transport selection, you would need to interact directly with NostrTransport.publish() or modify UnifiedPeerService, though this bypasses the app's privacy-optimized defaults.
What happens to queued messages if the app is force-quit?
According to the smart queuing implementation in UnifiedPeerService, messages persist in local storage and resume transmission on next app launch once connectivity is detected. The queue survives process termination.
Does BitChat ever use both transports simultaneously for redundancy?
No — the current implementation selects exactly one transport per message. Simultaneous dual-path transmission would increase relay load and complexity without significant reliability gains given the existing fallback and queuing mechanisms.
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 →