# How BitChat Chooses Between Bluetooth and Nostr for Its Dual Transport

> Discover how BitChat prioritizes Bluetooth mesh for direct P2P delivery and uses Nostr as a fallback. Learn about its dual transport strategy.

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

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatViewModelBootstrapper.swift) (lines 31-34), the bootstrapper assembles the transport array with **mesh first**:

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MessageRouter.swift) uses a simple **first-match-wins** algorithm in `reachableTransport(for:)` (lines 82-84):

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NostrTransport.swift) (lines 216-231) applies **stricter conditions**:

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

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) line 1116)
- **Nostr reachability** additionally requires active relay WebSocket ([`NostrTransport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouter.swift) for the `retryOutbox()` implementation details.