When Does BitChat Switch Between Bluetooth Mesh and Nostr Transports?

BitChat automatically selects Bluetooth mesh when available and falls back to Nostr only when Bluetooth is unavailable, with smart queuing when neither transport is reachable.

BitChat is an open-source, peer-to-peer messaging app that seamlessly bridges local Bluetooth mesh networks with the global Nostr protocol. Understanding its transport-switching logic helps developers build resilient messaging features and troubleshoot connectivity issues. This article explains exactly when and how BitChat switches between its two transport layers, based on the actual source code in the permissionlesstech/bitchat repository.

How BitChat's Transport Selection Works

The transport decision happens at message-send time through an intelligent priority system. The ChatViewModel class in bitchat/ViewModels/ChatViewModel.swift orchestrates this process, monitoring Bluetooth state and routing each message to the optimal path.

The selection follows three strict rules:

Priority Condition Transport Used
1 Bluetooth powered on and mesh link exists Bluetooth mesh — direct, Noise-encrypted, lowest latency
2 Bluetooth unavailable (powered off, unauthorized, no peer) Nostr fallback — relays via recipient's public key
3 Neither transport reachable Smart queuing — local storage until connectivity returns

This "Bluetooth First, Nostr Fallback, Smart Queue" policy is documented in the README at lines 87-95 and implemented throughout the core messaging stack.

The Core Switching Logic in ChatViewModel

The actual transport selection occurs in ChatViewModel.sendMessage(_:to:) and related methods. The view model maintains Bluetooth awareness through BluetoothStateReporting, updating its internal state via updateBluetoothState(_:) at line 1481.

Here's the simplified decision pattern from the source:

func sendMessage(_ payload: Data, to peer: Peer) async throws {
    // 1️⃣ Attempt Bluetooth mesh first
    if let mesh = meshService as? BluetoothStateReporting,
       mesh.getCurrentBluetoothState() == .poweredOn,
       let link = mesh.link(to: peer) {

        try await mesh.send(payload, over: link)          // Noise-encrypted mesh
        return
    }

    // 2️⃣ Fallback to Nostr network
    let nostrKey = peer.nostrPublicKey
    let envelope = BitChatPrivateEnvelope.encrypt(payload, for: nostrKey)
    try await nostrTransport.publish(envelope)          // Relay-based delivery
}

The showBluetoothAlert flag tracks UI state, but the critical routing decision depends on getCurrentBluetoothState() and whether mesh.link(to:) returns a valid connection.

Bluetooth Mesh: The Preferred Transport

BitChat always attempts Bluetooth Low Energy mesh first when conditions allow. This path provides:

  • Direct peer-to-peer delivery — no intermediate servers
  • Noise protocol encryption — cryptographic session establishment
  • Sub-100ms latency — locally routed packets

The BLE layer lives in bitchat/Transport/BLEService.swift, which manages CoreBluetooth state, mesh link formation, and encrypted packet exchange. A mesh link exists only when both devices have discovered each other through the Bluetooth mesh layer and completed the Noise handshake.

When Nostr Becomes the Fallback

Nostr activation triggers on any Bluetooth failure state:

  • poweredOff — Bluetooth disabled system-wide
  • unauthorized — user denied Bluetooth permissions
  • resetting — Bluetooth hardware temporarily unavailable
  • unsupported — device lacks Bluetooth capability
  • No reachable peer — recipient out of radio range or offline

In these cases, BitChatPrivateEnvelope.encrypt() wraps the payload for the recipient's Nostr public key, and NostrTransport.publish() relays it through configured Nostr relays in bitchat/Transport/NostrTransport.swift.

The Nostr path maintains privacy through app-specific envelope encryption in addition to Nostr's native encryption, ensuring BitChat messages remain unreadable to relay operators.

Smart Queuing for Offline Scenarios

When both transports fail — Bluetooth unavailable AND Nostr unreachable (no internet, relays down, or recipient has no Nostr identity) — BitChat does not drop the message.

Instead, the UnifiedPeerService in bitchat/Services/UnifiedPeerService.swift implements smart queuing: messages persist locally and automatically transmit once either transport becomes viable. This happens without user intervention; the app monitors state changes and drains the queue opportunistically.

Practical Code Examples

Checking Which Transport Would Be Used

Determine the active transport for a peer before sending:

import BitChat

func currentTransport(for peer: Peer) async -> TransportType {
    if let mesh = viewModel.meshService as? BluetoothStateReporting,
       mesh.getCurrentBluetoothState() == .poweredOn,
       mesh.link(to: peer) != nil {
        return .bluetoothMesh
    } else {
        return .nostr
    }
}

Returns .bluetoothMesh for local links, .nostr otherwise.

Sending With Automatic Transport Selection

Let BitChat choose the optimal path:

let text = "Hello from the local mesh!"
let data = text.data(using: .utf8)!

do {
    try await viewModel.sendMessage(data, to: targetPeer)
    print("Message dispatched via the optimal transport.")
} catch {
    print("Failed to send – will be queued until a transport becomes available.")
}

The sendMessage call transparently implements Bluetooth-first → Nostr-fallback → queue behavior.

Key Source Files

File Role
bitchat/ViewModels/ChatViewModel.swift Central view-model; monitors Bluetooth state via updateBluetoothState(_:) and routes messages
bitchat/Services/UnifiedPeerService.swift Unified abstraction over mesh and Nostr; implements fallback and queuing
bitchat/Transport/NostrTransport.swift Nostr relay connections, private-envelope encryption, publishing
bitchat/Transport/BLEService.swift CoreBluetooth management, mesh links, Noise-encrypted exchange
README.md (lines 87-95) High-level transport policy documentation

Summary

  • Bluetooth mesh is always preferred when powered on and a peer link exists — fastest and most private path
  • Nostr activates automatically when Bluetooth is unavailable (any failure state) or no mesh peer is reachable
  • Smart queuing handles total offline scenarios, delivering messages when connectivity returns
  • The switching logic lives in ChatViewModel.swift with BluetoothStateReporting monitoring and UnifiedPeerService abstraction
  • Developers interact with this system through simple sendMessage calls; transport selection happens transparently

Frequently Asked Questions

BitChat checks mesh.link(to: peer) in the BLEService layer — this returns a valid link only when the mesh layer has discovered the peer and completed the Noise protocol handshake. The check happens fresh on every send attempt, so link state changes are detected in real-time.

Can I force a message to use Nostr even if Bluetooth is available?

The public API in ChatViewModel follows the automatic selection policy. To override transport selection, you would need to interact directly with NostrTransport.publish() or modify UnifiedPeerService, though this bypasses the app's privacy-optimized defaults.

What happens to queued messages if the app is force-quit?

According to the smart queuing implementation in UnifiedPeerService, messages persist in local storage and resume transmission on next app launch once connectivity is detected. The queue survives process termination.

Does BitChat ever use both transports simultaneously for redundancy?

No — the current implementation selects exactly one transport per message. Simultaneous dual-path transmission would increase relay load and complexity without significant reliability gains given the existing fallback and queuing mechanisms.

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 →