How BitChat's Dual Transport Architecture Works: Bluetooth Mesh vs Nostr Routing
BitChat automatically prefers local Bluetooth-mesh delivery and falls back to Nostr relays only when the mesh cannot reach the recipient.
BitChat implements a dual transport system that seamlessly switches between peer-to-peer Bluetooth mesh and Internet-based Nostr protocols. This architecture ensures messages reach their destination through the fastest available path while maintaining functionality when devices are out of radio range. According to the permissionlesstech/bitchat source code, the routing logic centers on reachability detection and a prioritized transport list.
The Two Transport Layers
Bluetooth Mesh (BLEService)
The Bluetooth Low Energy mesh provides direct device-to-device communication without Internet infrastructure. It reports reachability when the Bluetooth stack observes a peer advertisement or maintains an active link.
In bitchat/Services/BLE/BLEService.swift, the reachability check appears at line 1116:
func isPeerReachable(_ peerID: PeerID) -> Bool {
// Returns true when peer is seen in mesh or within recent reachability window
}
This transport offers low latency and offline capability—ideal for proximity-based messaging.
Nostr (NostrTransport)
The Nostr transport bridges messages through Internet relays using WebSocket connections. Reachability requires both mesh visibility and an active relay connection, as implemented in bitchat/Services/NostrTransport.swift (lines 216-231):
func isPeerReachable(_ peerID: PeerID) -> Bool {
// Requires: mesh reports reachability AND at least one relay connected
guard meshService.isPeerReachable(peerID) else { return false }
return relayConnections.values.contains { $0.state == .connected }
}
Nostr serves as the fallback path when direct radio contact fails.
How MessageRouter Selects the Transport
The MessageRouter class in bitchat/Services/MessageRouter.swift implements the core decision logic through a simple, ordered scan.
Transport Priority Setup
The bootstrapper establishes priority at ChatViewModelBootstrapper.swift (lines 31-34):
let nostrTransport = NostrTransport(keychain: keychain, idBridge: idBridge)
nostrTransport.senderPeerID = meshService.myPeerID
let transports: [Transport] = [meshService, nostrTransport] // mesh first
let router = MessageRouter(transports: transports)
Order matters: mesh service appears first, making it the default when both paths are technically available.
Reachability-Based Selection
The selection algorithm at MessageRouter.swift (lines 82-84) returns the first reachable transport:
func reachableTransport(for peerID: PeerID) -> Transport? {
transports.first { $0.isPeerReachable(peerID) }
}
This one-liner drives the entire dual transport architecture:
- Scan the
transportsarray in declaration order - Return the first where
isPeerReachable(_:)returnstrue - Fall through to Nostr only if mesh reports unreachable
Prompt Delivery Guarantees
The base Transport protocol defines canDeliverPromptly(to:) at bitchat/Services/Transport.swift (lines 22-25):
func canDeliverPromptly(to peerID: PeerID) -> Bool {
isPeerReachable(peerID)
}
BLEService uses this default. NostrTransport overrides it to require live relay connectivity, ensuring the fallback path only activates when it can actually transmit.
Complete Routing Flow
When you send a message, this sequence executes:
- Check mesh reachability —
BLEService.isPeerReachable(_:)queries the Bluetooth stack - If reachable: Use BLEService for direct transmission
- If unreachable: Query
NostrTransport.isPeerReachable(_:)for relay availability - If Nostr reachable: Transmit via WebSocket to connected relays
- If neither reachable: Queue message in outbox for retry (handled by
MessageRouteroutbox logic)
Practical Implementation Example
The following pattern from the codebase demonstrates production usage:
// Bootstrap – order defines preference
let transports: [Transport] = [meshService, nostrTransport]
let router = MessageRouter(transports: transports)
// Send – routing is automatic
router.sendPrivate(
"Hey nearby!",
to: peerID,
recipientNickname: "Alice",
messageID: "msg-001"
)
The sendPrivate method internally calls reachableTransport(for:)—no manual transport selection required.
Key Source Files
| File | Purpose |
|---|---|
bitchat/ViewModels/ChatViewModelBootstrapper.swift |
Assembles [meshService, nostrTransport] array |
bitchat/Services/Transport.swift |
Defines Transport protocol and default canDeliverPromptly |
bitchat/Services/BLE/BLEService.swift |
Mesh reachability via Bluetooth stack |
bitchat/Services/NostrTransport.swift |
Nostr reachability requiring relay connection |
bitchat/Services/MessageRouter.swift |
Core routing with reachableTransport, fallback handling |
Summary
- Dual transport architecture combines
BLEService(local mesh) andNostrTransport(Internet relays) - Priority-based selection via ordered array—mesh always preferred when available
- Reachability testing determines path: Bluetooth stack state for mesh, relay connection state for Nostr
- Automatic fallback requires zero configuration;
MessageRouterhandles transparent switching - Outbox queuing preserves messages when no path exists, retrying when connectivity returns
Frequently Asked Questions
What determines whether BitChat uses Bluetooth or Nostr?
BitChat checks reachability in order: first BLEService.isPeerReachable(_:), then NostrTransport.isPeerReachable(_:). The mesh wins whenever the Bluetooth stack sees the target peer. Nostr only activates when mesh reports unreachable and at least one relay maintains a WebSocket connection.
Can BitChat send messages without Internet access?
Yes. Pure Bluetooth-mesh operation works entirely offline. The Nostr transport supplements but does not replace local radio communication. Messages queue locally when neither transport is available.
Why does Nostr reachability require mesh visibility?
NostrTransport.isPeerReachable(_:) calls meshService.isPeerReachable(peerID) as a prerequisite (lines 216-231, NostrTransport.swift). This design ensures Nostr serves as a range extender rather than a completely separate address space—you must at least know a peer exists in the mesh before attempting Internet delivery.
How does BitChat handle message delivery failures?
MessageRouter implements outbox logic that queues messages when reachableTransport(for:) returns nil. The router retries queued messages when transport state changes (new peer advertisement, relay connection established, etc.).
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 →