# How BitChat Selects Between Bluetooth Mesh and Nostr for Message Delivery

> Discover how BitChat intelligently selects between Bluetooth mesh and Nostr for message delivery. Learn about its runtime channel check for nearby and geohash-based communication.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: architecture
- Published: 2026-07-27

---

**BitChat uses a runtime channel check in `ChatViewModel` to automatically route messages through Bluetooth mesh for local "Nearby Only" channels or through Nostr relays for geohash-based location channels.**

BitChat implements a **dual‑transport architecture** that delivers chat messages over either a local Bluetooth LE mesh or the public Nostr relay network. The iOS client decides which path to use based on the active channel type and transport availability, with the selection logic centralized in the SwiftUI view model layer of the permissionlesstech/bitchat repository.

## Channel Types Drive Transport Selection

The transport decision begins in **`LocationChannelsModel`** ([`bitchat/ViewModels/LocationChannelsModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/LocationChannelsModel.swift)), which exposes a `selectedChannel` enum that categorizes conversations into two distinct modes:

- **`ChannelID.mesh`** – Represents the "Nearby Only" mode where devices communicate directly via Bluetooth mesh.
- **`ChannelID.location(geoHash)`** – Represents a geohash‑backed channel that routes messages through Nostr relays for broader, internet‑based delivery.

When a user switches channels in the UI, this enum value updates, signaling to the rest of the stack which transport layer should handle outgoing messages.

## Transport Routing Logic in ChatViewModel

The actual selection logic lives in **`ChatViewModel`** ([`bitchat/ViewModels/ChatViewModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatViewModel.swift)). When sending a message, the view model inspects `locationChannelsModel.selectedChannel` and invokes the corresponding transport:

```swift
func sendMessage(_ message: Message) async {
    switch locationChannelsModel.selectedChannel {
    case .mesh:
        await bleTransport.send(message)          // Bluetooth mesh path
    case .location(let geoHash):
        await nostrTransport.send(message, to: geoHash) // Nostr path
    }
}

```

This `switch` statement ensures that **messages are never sent to the wrong network**—mesh‑mode messages stay local, while location‑mode messages reach the global Nostr relay pool.

## Bluetooth Mesh Implementation

For local delivery, **`BLETransport`** interfaces with **`BLEService`** ([`bitchat/Services/BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLEService.swift)), a CoreBluetooth wrapper that manages the Bluetooth LE mesh stack. The service emits typed `TransportEvent` objects and handles peer discovery, connection state, and data fragmentation.

When `bleTransport.send(message)` is called, the payload is serialized and passed to the CoreBluetooth central manager:

```swift
let data = payload.toBLEData()
centralManager.sendData(data, to: peerPeripheral)

```

According to [`ARCHITECTURE_V2.md`](https://github.com/permissionlesstech/bitchat/blob/main/ARCHITECTURE_V2.md), this implementation supports the mesh‑branch diagram where devices forward messages hop‑by‑hop without internet connectivity.

## Nostr Relay Implementation

For geohash channels, **`NostrTransport`** ([`bitchat/Nostr/NostrTransport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrTransport.swift)) constructs cryptographically signed Nostr events using **`NostrIdentityBridge`**. It then delegates transmission to **`NostrRelayManager`** ([`bitchat/Nostr/NostrRelayManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrRelayManager.swift)), which maintains WebSocket connections to up to eight relays (clearnet or `.onion`).

The flow constructs a specific event kind and publishes it:

```swift
let event = try NostrEvent(
    kind: .chatMessage,
    content: payload.encoded,
    tags: [.geoHash(geoHash)],
    createdAt: Date()
)
try await relayManager.publish(event)

```

As documented in `TOR‑INTEGRATION.md`, when Tor is enabled, `NostrRelayManager` routes these WebSocket connections through the Tor proxy, adding a layer of network‑level privacy to the Nostr path.

## Fallback When Bluetooth Is Unavailable

If the Bluetooth stack reports an unavailable state—such as `CBManagerState.poweredOff` or `unauthorized`—the UI triggers `ChatBluetoothAlertPolicy` to notify the user. However, the architecture provides **implicit fallback behavior**: messages intended for mesh channels that cannot be delivered locally will still reach peers subscribed to the same conversation via Nostr, provided those peers are reachable through the relay network.

The `AppRuntime` (referenced in [`ARCHITECTURE_V2.md`](https://github.com/permissionlesstech/bitchat/blob/main/ARCHITECTURE_V2.md)) coordinates this by owning the Bluetooth manager, Tor session, and Nostr reconnection observers, ensuring that the correct transport remains active as network conditions change.

## Summary

- **Channel‑driven routing**: `LocationChannelsModel.selectedChannel` (`.mesh` vs `.location`) determines whether `ChatViewModel` uses Bluetooth or Nostr.
- **Bluetooth path**: Involves `BLETransport` → `BLEService` → CoreBluetooth for local mesh networking.
- **Nostr path**: Involves `NostrTransport` → `NostrRelayManager` → WebSocket (optionally via Tor) for global relay delivery.
- **Automatic selection**: The `switch` statement in `ChatViewModel.sendMessage` guarantees the correct transport is chosen at runtime without user intervention.
- **Resilient fallback**: Bluetooth unavailability triggers UI alerts but does not prevent message delivery to non‑mesh peers via Nostr.

## Frequently Asked Questions

### What happens if Bluetooth is disabled while I'm in a mesh channel?

If CoreBluetooth reports a `poweredOff` or `unauthorized` state, `ChatBluetoothAlertPolicy` displays a system alert prompting the user to enable Bluetooth. Messages will still fall back to the Nostr path for any peers who are subscribed to the conversation via relays, though local‑only mesh delivery will fail until Bluetooth is restored.

### Can BitChat use both transports simultaneously for the same message?

No. The architecture enforces mutual exclusivity at the transport layer. `ChatViewModel` evaluates `locationChannelsModel.selectedChannel` using a single `switch` statement, meaning a message is routed to either `BLETransport` **or** `NostrTransport`, but never both. This prevents duplicate delivery and separates local mesh traffic from global relay traffic.

### How does the app know which channel type is active?

The active channel is stored in **`LocationChannelsModel`** as a `ChannelID` enum value. When the user selects "Nearby Only" in the UI, the value becomes `.mesh`; when selecting a location‑based channel, it becomes `.location(geoHash)`. `ChatViewModel` observes this state and reconfigures its transport reference accordingly.

### Is Tor always used when sending Nostr messages?

Tor usage depends on the user's privacy settings as implemented in **`NostrRelayManager`**. When Tor is enabled, the manager routes WebSocket connections through the Tor proxy, allowing connections to `.onion` relays. If Tor is disabled, the manager connects directly to clearnet relays. This behavior is documented in `TOR‑INTEGRATION.md` and applies only to the Nostr transport path.