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 transports array in declaration order
  • Return the first where isPeerReachable(_:) returns true
  • 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:

  1. Check mesh reachability — BLEService.isPeerReachable(_:) queries the Bluetooth stack
  2. If reachable: Use BLEService for direct transmission
  3. If unreachable: Query NostrTransport.isPeerReachable(_:) for relay availability
  4. If Nostr reachable: Transmit via WebSocket to connected relays
  5. If neither reachable: Queue message in outbox for retry (handled by MessageRouter outbox 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) and NostrTransport (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; MessageRouter handles 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:

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 →