# When Does BitChat Switch Between Bluetooth Mesh and Nostr Transports?

> Discover when BitChat switches between Bluetooth mesh and Nostr transports. Learn how BitChat prioritizes Bluetooth and uses Nostr as a fallback for seamless communication.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-08-03

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/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:

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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:

```swift
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:

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatViewModel.swift) | Central view-model; monitors Bluetooth state via `updateBluetoothState(_:)` and routes messages |
| [`bitchat/Services/UnifiedPeerService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/UnifiedPeerService.swift) | Unified abstraction over mesh and Nostr; implements fallback and queuing |
| [`bitchat/Transport/NostrTransport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Transport/NostrTransport.swift) | Nostr relay connections, private-envelope encryption, publishing |
| [`bitchat/Transport/BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Transport/BLEService.swift) | CoreBluetooth management, mesh links, Noise-encrypted exchange |
| [`README.md`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/ChatViewModel.swift) with `BluetoothStateReporting` monitoring and `UnifiedPeerService` abstraction
- Developers interact with this system through simple `sendMessage` calls; transport selection happens transparently

## Frequently Asked Questions

### How does BitChat know if a Bluetooth mesh link exists?

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.