How BitChat Selects Between Bluetooth Mesh and Nostr for Message Delivery
BitChat uses a runtime channel check in ChatViewModel to automatically route messages through Bluetooth mesh for local "Nearby Only" channels or through Nostr relays for geohash-based location channels.
BitChat implements a dual‑transport architecture that delivers chat messages over either a local Bluetooth LE mesh or the public Nostr relay network. The iOS client decides which path to use based on the active channel type and transport availability, with the selection logic centralized in the SwiftUI view model layer of the permissionlesstech/bitchat repository.
Channel Types Drive Transport Selection
The transport decision begins in LocationChannelsModel (bitchat/ViewModels/LocationChannelsModel.swift), which exposes a selectedChannel enum that categorizes conversations into two distinct modes:
ChannelID.mesh– Represents the "Nearby Only" mode where devices communicate directly via Bluetooth mesh.ChannelID.location(geoHash)– Represents a geohash‑backed channel that routes messages through Nostr relays for broader, internet‑based delivery.
When a user switches channels in the UI, this enum value updates, signaling to the rest of the stack which transport layer should handle outgoing messages.
Transport Routing Logic in ChatViewModel
The actual selection logic lives in ChatViewModel (bitchat/ViewModels/ChatViewModel.swift). When sending a message, the view model inspects locationChannelsModel.selectedChannel and invokes the corresponding transport:
func sendMessage(_ message: Message) async {
switch locationChannelsModel.selectedChannel {
case .mesh:
await bleTransport.send(message) // Bluetooth mesh path
case .location(let geoHash):
await nostrTransport.send(message, to: geoHash) // Nostr path
}
}
This switch statement ensures that messages are never sent to the wrong network—mesh‑mode messages stay local, while location‑mode messages reach the global Nostr relay pool.
Bluetooth Mesh Implementation
For local delivery, BLETransport interfaces with BLEService (bitchat/Services/BLEService.swift), a CoreBluetooth wrapper that manages the Bluetooth LE mesh stack. The service emits typed TransportEvent objects and handles peer discovery, connection state, and data fragmentation.
When bleTransport.send(message) is called, the payload is serialized and passed to the CoreBluetooth central manager:
let data = payload.toBLEData()
centralManager.sendData(data, to: peerPeripheral)
According to ARCHITECTURE_V2.md, this implementation supports the mesh‑branch diagram where devices forward messages hop‑by‑hop without internet connectivity.
Nostr Relay Implementation
For geohash channels, NostrTransport (bitchat/Nostr/NostrTransport.swift) constructs cryptographically signed Nostr events using NostrIdentityBridge. It then delegates transmission to NostrRelayManager (bitchat/Nostr/NostrRelayManager.swift), which maintains WebSocket connections to up to eight relays (clearnet or .onion).
The flow constructs a specific event kind and publishes it:
let event = try NostrEvent(
kind: .chatMessage,
content: payload.encoded,
tags: [.geoHash(geoHash)],
createdAt: Date()
)
try await relayManager.publish(event)
As documented in TOR‑INTEGRATION.md, when Tor is enabled, NostrRelayManager routes these WebSocket connections through the Tor proxy, adding a layer of network‑level privacy to the Nostr path.
Fallback When Bluetooth Is Unavailable
If the Bluetooth stack reports an unavailable state—such as CBManagerState.poweredOff or unauthorized—the UI triggers ChatBluetoothAlertPolicy to notify the user. However, the architecture provides implicit fallback behavior: messages intended for mesh channels that cannot be delivered locally will still reach peers subscribed to the same conversation via Nostr, provided those peers are reachable through the relay network.
The AppRuntime (referenced in ARCHITECTURE_V2.md) coordinates this by owning the Bluetooth manager, Tor session, and Nostr reconnection observers, ensuring that the correct transport remains active as network conditions change.
Summary
- Channel‑driven routing:
LocationChannelsModel.selectedChannel(.meshvs.location) determines whetherChatViewModeluses Bluetooth or Nostr. - Bluetooth path: Involves
BLETransport→BLEService→ CoreBluetooth for local mesh networking. - Nostr path: Involves
NostrTransport→NostrRelayManager→ WebSocket (optionally via Tor) for global relay delivery. - Automatic selection: The
switchstatement inChatViewModel.sendMessageguarantees the correct transport is chosen at runtime without user intervention. - Resilient fallback: Bluetooth unavailability triggers UI alerts but does not prevent message delivery to non‑mesh peers via Nostr.
Frequently Asked Questions
What happens if Bluetooth is disabled while I'm in a mesh channel?
If CoreBluetooth reports a poweredOff or unauthorized state, ChatBluetoothAlertPolicy displays a system alert prompting the user to enable Bluetooth. Messages will still fall back to the Nostr path for any peers who are subscribed to the conversation via relays, though local‑only mesh delivery will fail until Bluetooth is restored.
Can BitChat use both transports simultaneously for the same message?
No. The architecture enforces mutual exclusivity at the transport layer. ChatViewModel evaluates locationChannelsModel.selectedChannel using a single switch statement, meaning a message is routed to either BLETransport or NostrTransport, but never both. This prevents duplicate delivery and separates local mesh traffic from global relay traffic.
How does the app know which channel type is active?
The active channel is stored in LocationChannelsModel as a ChannelID enum value. When the user selects "Nearby Only" in the UI, the value becomes .mesh; when selecting a location‑based channel, it becomes .location(geoHash). ChatViewModel observes this state and reconfigures its transport reference accordingly.
Is Tor always used when sending Nostr messages?
Tor usage depends on the user's privacy settings as implemented in NostrRelayManager. When Tor is enabled, the manager routes WebSocket connections through the Tor proxy, allowing connections to .onion relays. If Tor is disabled, the manager connects directly to clearnet relays. This behavior is documented in TOR‑INTEGRATION.md and applies only to the Nostr transport path.
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 →