# How BitChat's Dual Transport Architecture Works: Bluetooth Mesh vs Nostr Routing

> Discover how BitChat's dual transport architecture efficiently combines Bluetooth mesh and Nostr relays for seamless local and remote messaging. Learn its fallback strategy.

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

---

**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](https://github.com/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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift), the reachability check appears at line 1116:

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

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MessageRouter.swift) implements the core decision logic through a simple, ordered scan.

### Transport Priority Setup

The bootstrapper establishes priority at [`ChatViewModelBootstrapper.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatViewModelBootstrapper.swift) (lines 31-34):

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouter.swift) (lines 82-84) returns the first reachable transport:

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/Transport.swift) (lines 22-25):

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

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatViewModelBootstrapper.swift) | Assembles `[meshService, nostrTransport]` array |
| [`bitchat/Services/Transport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/Transport.swift) | Defines `Transport` protocol and default `canDeliverPromptly` |
| [`bitchat/Services/BLE/BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift) | Mesh reachability via Bluetooth stack |
| [`bitchat/Services/NostrTransport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NostrTransport.swift) | Nostr reachability requiring relay connection |
| [`bitchat/Services/MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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.).