How BitChat's Dual Transport Architecture Balances Bluetooth Mesh and Nostr

BitChat's dual transport architecture prioritizes Bluetooth mesh for local peer-to-peer messaging and automatically falls back to Nostr relays when the mesh cannot reach the recipient, with the MessageRouter selecting the first available transport based on real-time reachability tests.

BitChat, an open-source messaging application from permissionlesstech/bitchat, implements a sophisticated dual transport architecture that combines local Bluetooth mesh networking with global Nostr relay infrastructure. This hybrid approach ensures messages reach their destination through the most efficient path available, prioritizing direct peer-to-peer connections over internet-based relays. Understanding how BitChat's dual transport architecture functions reveals a clever prioritization system that minimizes latency while maintaining connectivity across diverse network conditions.

Transport Layer Components

BitChat ships with two independent transport implementations that serve distinct networking purposes:

  • Bluetooth Mesh (BLEService): A peer-to-peer radio mesh enabling direct device-to-device communication without internet connectivity. This transport reports reachability when the Bluetooth stack observes a peer link or recent advertisement.
  • Nostr (NostrTransport): An internet-backed relay network requiring an active WebSocket connection to at least one Nostr relay to be considered reachable.

The system evaluates these transports in sequence, always attempting the local mesh first before consuming internet bandwidth.

How MessageRouter Selects the Transport

The routing decision logic resides in MessageRouter.swift, which orchestrates transport selection through ordered reachability testing.

Transport Priority Order

The bootstrapper establishes transport precedence during initialization. In ChatViewModelBootstrapper.swift (lines 31-34), the code creates an ordered array placing the mesh service first:

let transports: [Transport] = [meshService, nostrTransport]
let router = MessageRouter(transports: transports)

This array order determines selection priority. The router iterates through this list sequentially, making Bluetooth mesh the preferred path whenever available.

Reachability Testing Implementation

The core selection method reachableTransport(for:) in MessageRouter.swift (lines 82-84) implements a simple but effective filtering strategy:

func reachableTransport(for peerID: PeerID) -> Transport? {
    return transports.first { $0.isPeerReachable(peerID) }
}

This method returns the first transport reporting reachability for the target peer, enabling automatic fallback if the primary transport fails.

Bluetooth Mesh Reachability

BLEService.isPeerReachable(_:) determines mesh availability by checking the Bluetooth stack state. According to BLEService.swift (line 1116), the method returns true when the peer is actively observed or was seen within the mesh reachability window. This ensures recent proximity triggers mesh selection even if the link fluctuates momentarily.

Nostr Relay Requirements

NostrTransport.isPeerReachable(_:) imposes stricter connectivity requirements. As implemented in NostrTransport.swift (lines 216-231), this method returns true only when the mesh reports reachability and at least one Nostr relay maintains an active WebSocket connection. This dual requirement prevents attempting relay delivery when internet connectivity is absent.

Additionally, Transport.swift (lines 22-25) defines a default canDeliverPromptly(to:) implementation that forwards to isPeerReachable, ensuring mesh-reachable peers are marked as prompt delivery candidates. Nostr overrides this to require the live relay connection.

Message Routing Flow and Fallback Behavior

When sending a message, the router executes a deterministic fallback sequence:

  1. Check if target is mesh-reachable via BLEService (first in array)
  2. If mesh fails, verify Nostr relay connectivity via NostrTransport
  3. If neither transport reaches the peer, queue the message for later retry using the outbox logic in MessageRouter.swift

This architecture guarantees low-latency local delivery when possible, transparently bridging to the internet only when necessary.

Example implementation:

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

// Send with automatic transport selection
router.sendPrivate("Hey nearby!", to: peerID, recipientNickname: "Alice", messageID: "msg-001")
// Router selects BLEService if peer is nearby, otherwise falls back to Nostr

Summary

BitChat's dual transport architecture delivers robust messaging through intelligent path selection:

  • Priority-based routing: The [meshService, nostrTransport] array order ensures Bluetooth mesh always takes precedence
  • Real-time reachability: MessageRouter.reachableTransport(for:) evaluates isPeerReachable on each transport sequentially
  • Mesh-first local delivery: BLEService provides direct peer-to-peer connectivity without internet dependency
  • Nostr fallback: Internet relay activates only when mesh reachability fails or the peer requires remote connectivity
  • Automatic queuing: Messages queue for retry when neither transport reports reachability

Frequently Asked Questions

How does BitChat decide between Bluetooth and Nostr for message delivery?

BitChat evaluates transports in a fixed priority order defined in ChatViewModelBootstrapper.swift. The MessageRouter checks isPeerReachable for each transport sequentially, selecting the first that returns true. Bluetooth mesh appears first in the array, so the system attempts local delivery before checking Nostr relay connectivity.

What happens if a peer is reachable on both Bluetooth mesh and Nostr?

The Bluetooth mesh transport always wins when both are available because MessageRouter.reachableTransport(for:) uses transports.first to select the initial match. Since ChatViewModelBootstrapper.swift initializes the array with mesh first, messages route through the local radio rather than consuming internet bandwidth.

Can BitChat send messages when neither transport is reachable?

No, but the system implements outbox queuing in MessageRouter.swift. If reachableTransport(for:) returns nil for both BLEService and NostrTransport, the message enters a retry queue. The router attempts redelivery automatically when either transport subsequently reports reachability for the target peer.

Does Nostr require an active internet connection to be considered reachable?

Yes. According to NostrTransport.swift lines 216-231, isPeerReachable returns true only when the app maintains an active WebSocket connection to at least one Nostr relay. Without this connection, the transport reports unreachable, forcing the router to queue the message or wait for mesh connectivity.

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 →