How BitChat Routes Messages Between Bluetooth and Nostr Transports
BitChat routes messages by abstracting Bluetooth Low-Energy (BLE) and Nostr networks behind a unified Transport protocol, using peer capability flags to determine whether to send via local BLE mesh or remote Nostr relays.
The permissionlesstech/bitchat repository implements a hybrid messaging system that seamlessly switches between local Bluetooth mesh networks and decentralized Nostr relays. This article examines the capability-driven routing mechanism that enables intelligent transport selection without exposing complexity to the user interface layer.
The Transport Abstraction Layer
BitChat centralizes all network communication behind a single Transport protocol defined in bitchat/Services/Transport.swift【1†L31-L57】. This protocol exposes a common interface for sending messages, querying peer capabilities, and checking delivery feasibility.
Two concrete implementations conform to this protocol:
BLEService– Manages the Bluetooth Low-Energy mesh network through an extension declared at the end ofTransport.swift【1†L107-L113】NostrTransport– Handles communication with Nostr relays via a separate extension inNostrTransport.swift
Both implementations override the default protocol methods to report their specific delivery capabilities and reachability status.
Capability-Driven Routing Logic
The routing decision depends on peer capabilities advertised during discovery. In localPackages/BitFoundation/Sources/BitFoundation/PeerCapabilities.swift, peers advertise a PeerCapabilities value that includes flags such as .nostrBridge.
When the Chat layer (specifically ChatViewModel or ChatTransportEventCoordinator) prepares to send a message, it executes the following capability lookup:
-
Query capabilities – Call
transport.peerCapabilities(peerID)to retrieve the peer's advertised features. The default implementation inTransport.swiftreturns an empty set, while concrete transports override this to return actual detected capabilities. -
Evaluate delivery feasibility – Invoke
transport.canDeliverPromptly(to:)to determine reachability. The BLE transport returnstruefor peers within the local mesh, while the Nostr transport returnstrueonly for peers identified by Nostr public keys. -
Dispatch to appropriate transport – Based on the capability check, the message routes through either
BLEService.sendMessage(...)for local mesh delivery orNostrTransport.sendMessage(...)for remote relay transmission.
Message Flow Implementation
The following Swift patterns demonstrate how BitChat implements transport-agnostic routing while maintaining transport-specific optimizations.
Checking Peer Capabilities
Before sending, the view-model inspects the destination peer's supported protocols:
// Query the transport for peer capabilities
let caps = transport.peerCapabilities(peerID)
// Route based on capability flags
if caps.contains(.nostrBridge) {
// Target is a Nostr bridge peer; NostrTransport handles delivery
transport.sendMessage("Hello via Nostr", mentions: [])
} else {
// Target available via local BLE mesh
transport.sendMessage("Hello via BLE", mentions: [])
}
Unified Event Handling
All transports emit events through a common TransportEvent enum (defined in Transport.swift), which includes cases such as .messageReceived and .bluetoothStateUpdated. The ChatTransportEventCoordinator (located in bitchat/ViewModels/ChatTransportEventCoordinator.swift) subscribes to these events and routes them to the UI layer without revealing the underlying medium:
extension ChatViewModel: TransportEventDelegate {
func didReceiveTransportEvent(_ event: TransportEvent) {
switch event {
case .messageReceived(let msg):
// Handles messages from either BLE or Nostr uniformly
handleIncomingMessage(msg)
case .bluetoothStateUpdated(let state):
// Only BLEService emits this; NostrTransport never does
updateBluetoothUI(state)
default:
break
}
}
}
This architecture ensures that Bluetooth-specific state updates (like radio power changes) only propagate when relevant, while message reception remains entirely transport-agnostic.
Key Files and Architecture
The routing mechanism spans several critical files in the permissionlesstech/bitchat repository:
| File | Role |
|---|---|
bitchat/Services/Transport.swift |
Defines the Transport protocol, default implementations, and reachability helpers |
bitchat/Services/NostrTransport.swift |
Concrete Nostr implementation that overrides capability checks for bridge peers |
bitchat/Services/BLEService.swift |
BLE mesh implementation conforming to Transport via protocol extension |
bitchat/ViewModels/ChatTransportEventCoordinator.swift |
Central event router that listens for TransportEvent emissions |
localPackages/BitFoundation/Sources/BitFoundation/PeerCapabilities.swift |
Defines capability flags including .nostrBridge that drive routing decisions |
Summary
- BitChat uses a protocol-oriented abstraction where both BLE and Nostr networks implement the same
Transportinterface. - Routing decisions rely on capability flags queried via
peerCapabilities(), specifically checking for.nostrBridgeto identify Nostr-capable peers. - Delivery feasibility is determined by
canDeliverPromptly(to:), allowing the system to prefer local Bluetooth mesh when available and fall back to Nostr relays for remote peers. - Event handling is unified through the
TransportEventenum andChatTransportEventCoordinator, keeping transport details encapsulated from the UI layer.
Frequently Asked Questions
How does BitChat determine if a peer supports Nostr?
BitChat examines the PeerCapabilities structure advertised during peer discovery. If the capabilities set contains .nostrBridge (defined in PeerCapabilities.swift), the peer acts as a bridge between the local BLE mesh and the Nostr relay network. The transport.peerCapabilities(peerID) method retrieves these flags, with each concrete transport overriding the default implementation to return accurate capability data.
What happens if a peer has both Bluetooth and Nostr capabilities?
The routing logic prioritizes based on delivery feasibility rather than capability presence alone. The system calls canDeliverPromptly(to:) on the available transports. If the peer is reachable via BLE (within radio range), the BLE transport returns true and handles delivery. If the peer is only reachable via Nostr (out of Bluetooth range), the Nostr transport returns true for that specific peer identifier, and the message routes through the relay network.
How does the Transport protocol unify BLE and Nostr events?
Both transport implementations emit events through the TransportEvent enum, which includes generic cases like .messageReceived alongside transport-specific cases like .bluetoothStateUpdated. The ChatTransportEventCoordinator subscribes to these events from all active transports and dispatches them to view-models. This allows the UI to handle incoming messages identically regardless of origin, while still responding to Bluetooth-specific state changes when necessary.
Where is the routing decision made in the codebase?
The decision logic resides in the chat layer, specifically within ChatViewModel or ChatTransportEventCoordinator. These components query capabilities via the Transport protocol interface, evaluate reachability using canDeliverPromptly(to:), and then invoke sendMessage on the appropriate concrete implementation. The transports themselves expose uniform APIs but implement transport-specific delivery logic internally.
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 →