How BitChat's Dual Transport Architecture Routes Messages Between Bluetooth Mesh and Nostr

BitChat routes messages through Bluetooth mesh first when peers are locally reachable, and automatically falls back to Nostr relays only when the mesh cannot reach the destination.

BitChat implements a unique dual-transport architecture that combines Bluetooth Low Energy mesh networking with the Nostr protocol to enable censorship-resistant messaging. This architecture intelligently selects between local peer-to-peer radio transmission and Internet-based relay networks based on real-time reachability detection. Understanding how BitChat's dual transport architecture handles message routing reveals why the app prioritizes low-latency local connections while maintaining global connectivity through automatic Nostr fallback.

Overview of the Two Transport Layers

BitChat ships with two independent transport implementations that serve different connectivity scenarios.

Bluetooth Mesh (BLEService)

The BLEService provides a peer-to-peer radio mesh capable of directly reaching nearby devices without Internet infrastructure. This transport reports reachability when the Bluetooth stack observes a link or recent advertisement from the target peer. According to the source code in bitchat/Services/BLE/BLEService.swift (line 1116), isPeerReachable(_:) returns true when the device has been seen within the mesh reachability window.

Nostr Transport (NostrTransport)

The NostrTransport acts as an Internet-backed relay network. As implemented in bitchat/Services/NostrTransport.swift (lines 216-231), this transport only reports reachability when two conditions are met: the mesh reports the peer as reachable and the client maintains an active WebSocket connection to at least one Nostr relay. This ensures Nostr serves strictly as a fallback mechanism rather than the default path.

Message Routing Logic and Transport Selection

The core routing decision flow lives in MessageRouter.swift. The architecture follows a priority-based selection process that evaluates transports in a specific order.

Transport Priority Configuration

The bootstrapper establishes the preference order when initializing the chat view model. In bitchat/ViewModels/ChatViewModelBootstrapper.swift (lines 31-34), the code creates the transport array with mesh listed first:

let nostrTransport = NostrTransport(keychain: keychain, idBridge: idBridge)
nostrTransport.senderPeerID = meshService.myPeerID
let transports: [Transport] = [meshService, nostrTransport]   // mesh first
let router = MessageRouter(transports: transports)

This array order is critical because MessageRouter.reachableTransport(for:) (MessageRouter.swift lines 82-84) returns the first transport whose isPeerReachable(_:) method returns true.

Reachability Detection

Each transport implements reachability differently based on its underlying technology:

  • BLE Reachability: BLEService.isPeerReachable(_:) checks if the Bluetooth stack currently sees the peer or if the peer was observed recently within the mesh window.
  • Nostr Reachability: NostrTransport.isPeerReachable(_:) validates both mesh visibility and relay connectivity, ensuring Internet fallback only occurs when infrastructure is actually available.

Prompt Delivery Requirements

The default implementation of canDeliverPromptly(to:) in bitchat/Services/Transport.swift (lines 22-25) simply forwards the reachability test. This means a mesh-reachable peer is automatically considered "prompt." Nostr overrides this behavior to additionally require a live relay connection, preventing attempts to send via disconnected relays.

Practical Message Routing Example

When sending a private message, the routing logic remains transparent to the caller but executes the priority selection behind the scenes:

// Sending a private message – routing logic hidden inside MessageRouter
router.sendPrivate("Hey nearby!", to: peerID, recipientNickname: "Alice", messageID: "msg-001")
/* 
   - reachableTransport(for:) scans `transports` in order.
   - If `meshService.isPeerReachable(peerID)` → BLEService is used.
   - Otherwise, if `nostrTransport.isPeerReachable(peerID)` (relay connected) → Nostr.
   - If both fail, the message is queued for later retry.
*/

This implementation ensures that if the target peer is Bluetooth-mesh reachable, the router selects BLEService and transmits directly over the local mesh. If the peer is not mesh-reachable but the app has an active Nostr relay connection, the router falls back to NostrTransport. When neither transport can reach the peer, MessageRouter queues the message for later retry using its internal outbox logic.

Summary

  • BitChat's dual transport architecture prioritizes local Bluetooth mesh connections over Internet-based Nostr relays to minimize latency and maximize privacy.
  • Transport selection relies on an ordered array [meshService, nostrTransport] where the first reachable transport wins.
  • Reachability detection differs by transport: BLE checks for radio visibility while Nostr requires both visibility and active relay WebSocket connections.
  • MessageRouter handles automatic fallback and queuing, ensuring messages deliver via the fastest available path or retry later when offline.
  • Key files driving this behavior include ChatViewModelBootstrapper.swift (configuration), MessageRouter.swift (routing logic), and the respective transport implementations in BLEService.swift and NostrTransport.swift.

Frequently Asked Questions

How does BitChat decide whether to use Bluetooth mesh or Nostr?

BitChat evaluates transports in the order defined in ChatViewModelBootstrapper.swift: Bluetooth mesh first, then Nostr. The MessageRouter calls reachableTransport(for:) which returns the first transport where isPeerReachable(_:) returns true. This means Bluetooth always wins when both are technically available, ensuring local delivery preferred over Internet relay.

What happens if a peer is reachable on both transports?

When a peer is visible on both Bluetooth mesh and Nostr relays, BitChat selects Bluetooth mesh because it appears first in the transports array. This design minimizes Internet dependency and reduces latency by using direct radio links rather than routing through remote relays.

How does BitChat handle messages when no transport can reach the recipient?

When reachableTransport(for:) returns nil—meaning neither BLE nor Nostr reports reachability—the MessageRouter queues the message in an outbox for later retry. The system attempts redelivery when the recipient becomes reachable via either transport, ensuring messages survive temporary connectivity gaps.

Why does NostrTransport require mesh reachability to report itself as reachable?

As implemented in NostrTransport.swift lines 216-231, the isPeerReachable(_:) method checks both mesh visibility and relay connectivity. This dependency ensures that Nostr acts strictly as a bridge extension of the mesh rather than an independent discovery mechanism, maintaining the architecture's design principle that local mesh visibility governs all routing decisions.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →