How BitChat Chooses Between Bluetooth and Nostr for Its Dual Transport

BitChat prefers Bluetooth mesh for direct peer-to-peer delivery and falls back to Nostr only when the mesh cannot reach the target peer.

BitChat implements a dual-transport architecture that seamlessly switches between local Bluetooth mesh networking and Internet-based Nostr relays. This design prioritizes low-latency, censorship-resistant local communication while maintaining global reachability through Nostr's decentralized relay network. Understanding how BitChat makes this transport selection requires examining the MessageRouter implementation and the protocol contracts defined across its service layer.

Transport Layer Overview

BitChat ships with two independent transport implementations defined in bitchat/Services/Transport.swift:

Transport Protocol Role Reachability Condition
BLEService Transport Bluetooth link or recent advertisement observed
NostrTransport Transport Active WebSocket connection to ≥1 Nostr relay plus mesh reachability check

Both transports conform to the Transport protocol, which requires isPeerReachable(_:) and canDeliverPromptly(to:) methods. The default canDeliverPromptly implementation simply forwards to isPeerReachable (Transport.swift lines 22-25), making reachability the primary decision signal.

How the Router Prioritizes Bluetooth Over Nostr

The transport preference is hardcoded in initialization order. In bitchat/ViewModels/ChatViewModelBootstrapper.swift (lines 31-34), the bootstrapper assembles the transport array with mesh first:

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

This array order is critical. The MessageRouter in bitchat/Services/MessageRouter.swift uses a simple first-match-wins algorithm in reachableTransport(for:) (lines 82-84):

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

By placing meshService at index 0, any Bluetooth-reachable peer automatically triggers mesh delivery before Nostr is even evaluated.

Reachability Detection: BLEService vs NostrTransport

Bluetooth Mesh Reachability

BLEService.isPeerReachable(_:) in bitchat/Services/BLE/BLEService.swift (line 1116) returns true when:

  • The Bluetooth stack currently sees the peer, or
  • The peer was observed within the mesh reachability window

This enables rapid detection of nearby devices without requiring active connections.

Nostr Reachability (Strict Relay Requirement)

NostrTransport.isPeerReachable(_:) in bitchat/Services/NostrTransport.swift (lines 216-231) applies stricter conditions:

func isPeerReachable(_ peerID: PeerID) -> Bool {
    // Requires BOTH mesh awareness AND active relay connection
    guard let meshService = meshService,
          meshService.isPeerReachable(peerID) else {
        return false
    }
    
    // Must have live WebSocket to at least one relay
    guard !relayConnections.isEmpty else {
        return false
    }
    
    return true
}

Notably, Nostr reachability depends on mesh reachability plus relay connectivity. This prevents Nostr from being selected merely because the Internet is available—the peer must still be known to the mesh layer.

The Complete Message Flow

When router.sendPrivate(_:to:recipientNickname:messageID:) is invoked, the following cascade executes:

  1. Reachability scan – reachableTransport(for:) iterates [meshService, nostrTransport]
  2. BLE match – if BLEService.isPeerReachable returns true, message transmits immediately via Bluetooth mesh
  3. Nostr fallback – if BLE is false but NostrTransport.isPeerReachable is true (relay connected), message routes through Nostr
  4. Outbox queue – if neither transport reports reachability, MessageRouter queues for retry

This fallback chain ensures zero-configuration local delivery when possible, with transparent Internet bridging only when necessary.

Code Example: Observing Transport Selection

// Simulating transport selection for a peer
let peerID = PeerID(identifier: "alice-device-001")

// Check what the router would select
if let transport = router.reachableTransport(for: peerID) {
    print("Selected transport: \(type(of: transport))")
    // → BLEService if nearby, NostrTransport if distant but relay-connected
} else {
    print("No reachability — message queued to outbox")
}

Why This Architecture Matters

Design Decision Benefit
Mesh-first ordering Minimizes Internet metadata leaks; works offline
Relay-gated Nostr reachability Prevents spurious relay connections; conserves battery
Protocol abstraction Transport interface allows adding future transports (WiFi Direct, etc.) without router changes
Explicit outbox Guaranteed delivery when intermittent connectivity resolves

Summary

  • Transport priority is set in ChatViewModelBootstrapper.swift via array order: [meshService, nostrTransport]
  • Selection logic lives in MessageRouter.reachableTransport(for:) using first { $0.isPeerReachable(peerID) }
  • BLE reachability requires only Bluetooth stack visibility (BLEService.swift line 1116)
  • Nostr reachability additionally requires active relay WebSocket (NostrTransport.swift lines 216-231)
  • Fallback behavior transparently promotes queued messages when connectivity changes

Frequently Asked Questions

What happens if both Bluetooth and Nostr are available?

BitChat always chooses Bluetooth when the peer is mesh-reachable. The transports array ordering in ChatViewModelBootstrapper.swift hardcodes this preference, and MessageRouter.reachableTransport(for:) returns the first match.

Can I force Nostr even if Bluetooth shows reachability?

Not through the public API. The MessageRouter has no override mechanism; transport selection is fully automated based on isPeerReachable(_:) results. To modify behavior, you would need to reorder the transports array or implement a custom Transport wrapper.

Why does Nostr reachability require mesh reachability too?

NostrTransport.swift guards relay-based delivery with meshService.isPeerReachable(peerID) (line 218). This prevents sending Nostr messages to unknown peers—ensuring the routing layer never attempts delivery to recipients not in your contact graph, even if relays are connected.

Does message delivery fail silently when neither transport is reachable?

No. MessageRouter implements outbox queuing: messages are persisted and automatically retried when reachabilityChanged notifications fire or transports report new peer visibility. Check MessageRouter.swift for the retryOutbox() implementation details.

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 →