# How BitChat Handles Message Routing Fallback from Bluetooth to Nostr

> Discover how BitChat routes messages seamlessly from Bluetooth to Nostr. Learn about its fallback mechanism that ensures message delivery even when peers are offline.

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

---

**BitChat uses a transport-agnostic routing layer that prioritizes Bluetooth mesh connections and automatically falls back to Nostr relays when BLE peers are unreachable, depositing encrypted messages via bridge couriers for later retrieval.**

BitChat, an open-source peer-to-peer messenger from permissionlesstech/bitchat, implements a resilient message routing system designed for offline-first communication. When Bluetooth Low Energy (BLE) mesh connections fail or peers move out of range, the application seamlessly transitions traffic to Nostr relays without user intervention. This article examines the Swift implementation details behind BitChat's transport fallback mechanism, focusing on how the `MessageRouter` coordinates between `BLEService` and `NostrTransport` to guarantee message delivery.

## Transport-Agnostic Architecture

The routing system centers on a protocol-based transport layer. BitChat maintains an array of objects conforming to the `Transport` protocol, typically containing a `BLEService` instance for local mesh networking and a `NostrTransport` instance for internet-based relay communication.

```swift
let transports: [Transport] = [bleService, nostrTransport]

```

This design allows `MessageRouter` to evaluate availability dynamically rather than hardcoding network preferences.

## Router Decision Logic and Fallback Triggers

Located in [`bitchat/Services/MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MessageRouter.swift), the `sendPrivate(_:to:recipientNickname:messageID:)` method implements a three-tier selection process:

1. **Connected Transport**: Check for an active secure session via `connectedTransport(for:)` and `canDeliverSecurely(to:)`
2. **Reachable Transport**: If no active session exists, verify BLE mesh availability using `reachableTransport(for:)` and `BLEService.isPeerReachable`
3. **Nostr Fallback**: When BLE returns false, trigger `attemptCourierDeposit(messageID:for:)` to queue the message for Nostr relay deposit

The router distinguishes between "connected" (active session) and "reachable" (in range but no session) states. When `canDeliverPromptly(to:)` returns false or no BLE transport is reachable, the system initiates the Nostr fallback path.

### Code Implementation of Transport Selection

```swift
func sendPrivate(_ content: String,
                 to peerID: PeerID,
                 recipientNickname: String,
                 messageID: String) {
    // 1️⃣ Try a connected transport with a secure session (BLE or Nostr)
    if let transport = connectedTransport(for: peerID),
       transport.canDeliverSecurely(to: peerID) {
        enqueue(message, for: peerID)
        transport.sendPrivateMessage(content,
                                     to: peerID,
                                     recipientNickname: recipientNickname,
                                     messageID: messageID)
        return
    }

    // 2️⃣ Try a reachable transport (BLE mesh is still in range)
    if let transport = reachableTransport(for: peerID) {
        enqueue(message, for: peerID)
        transport.sendPrivateMessage(content,
                                     to: peerID,
                                     recipientNickname: recipientNickname,
                                     messageID: messageID)

        // When the reachable transport cannot deliver promptly,
        // hand the sealed envelope to Nostr couriers as a fallback.
        if !transport.canDeliverPromptly(to: peerID) {
            attemptCourierDeposit(messageID: messageID, for: peerID)
        }
        return
    }

    // 3️⃣ No BLE link at all → rely on Nostr (courier deposit already queued)
    var unsent = message
    unsent.sendAttempts = 0
    enqueue(unsent, for: peerID)
    attemptCourierDeposit(messageID: messageID, for: peerID)
}

```

## Nostr Courier Deposit and Bridge Mechanism

When BLE cannot deliver, `attemptCourierDeposit` transfers the sealed envelope to Nostr infrastructure. This process involves two key components: the bridge deposit logic and the gateway publishing service.

The `bridgeCourierDeposit` method writes the message to configured Nostr relays. Upon successful acknowledgment, the UI receives a "carried" callback indicating the message persists on the internet for later retrieval by the recipient.

### BLE-to-Nostr Bridge Callback

[`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) exposes a closure-based bridge mechanism via `onNostrCarrierPacket`. This allows mesh-originated payloads to exit the local network through Nostr when the mesh cannot route to the final destination.

```swift
// Inside BLEService
var onNostrCarrierPacket: (@MainActor (_ payload: Data,
                                         _ from: PeerID,
                                         _ directedToUs: Bool) -> Void)?

// When a mesh announce contains a Nostr‑carrier payload
private func handleNostrCarrier(_ payload: Data,
                                 from senderID: PeerID,
                                 directedToUs: Bool) {
    // Forward the payload to the Nostr side of the app
    self?.onNostrCarrierPacket?(payload, senderID, directedToUs)
}

```

### Gateway Service Integration

In [`ChatViewModelBootstrapper.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatViewModelBootstrapper.swift), the bridge closure wires directly to `GatewayService.publishToRelays`:

```swift
// ChatViewModelBootstrapper.swift – during app start‑up
bleService.onNostrCarrierPacket = { payload, from, directedToUs in
    // `gateway` is a singleton that knows how to publish to Nostr relays
    gateway.publishToRelays(payload, from: from, directedToUs: directedToUs)
}

```

[`GatewayService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/GatewayService.swift) handles the protocol translation, creating signed Nostr events that carry the mesh payload:

```swift
func publishToRelays(_ payload: Data,
                     from sender: PeerID,
                     directedToUs: Bool) {
    // Create a signed Nostr event that carries the mesh payload
    let event = NostrProtocol.createGatewayEvent(payload,
                                                 senderPubkey: idBridge.npub,
                                                 geohash: currentGeohash())
    // Hand the event to the Nostr transport (which writes to relays)
    nostrTransport.publish(event)
}

```

## Gateway Mode and Retry Logic

BitChat includes a user-togglable **gateway mode** (`.gateway` bit) that automatically publishes undeliverable mesh messages to Nostr using per-geohash identities. This ensures mesh-only traffic reaches internet-connected peers without manual intervention.

For reliability, `MessageRouter` maintains an outbox for pending Nostr deposits. The `retryBridgeCourierDeposits` method performs periodic sweeps to re-attempt delivery when relays experience temporary outages (such as Tor startup delays), continuing until acknowledgment or message TTL expiration.

## Summary

- BitChat implements a **transport-agnostic routing layer** in [`MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouter.swift) that maintains both BLE and Nostr transport instances
- The router prioritizes **connected** BLE sessions, falls back to **reachable** mesh peers, then defaults to **Nostr relays** via `attemptCourierDeposit`
- **Bridge couriers** allow mesh-originated messages to exit through Nostr using the `onNostrCarrierPacket` closure wired in [`ChatViewModelBootstrapper.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatViewModelBootstrapper.swift)
- **GatewayService** handles protocol translation, signing mesh payloads as Nostr events with geohash-based identities
- **Automatic retry logic** via `retryBridgeCourierDeposits` ensures eventual delivery despite temporary relay unavailability

## Frequently Asked Questions

### What happens if both Bluetooth and Nostr relays are unavailable?

If no transport is reachable, BitChat retains the message in the local outbox and periodically retries via `MessageRouter.retryBridgeCourierDeposits`. The system continues attempting delivery until the message's TTL expires or a transport (BLE or Nostr) becomes available.

### How does BitChat decide between BLE mesh and Nostr for message delivery?

The `MessageRouter` checks `connectedTransport(for:)` first for active secure sessions, then `reachableTransport(for:)` to detect in-range BLE peers. Only when `BLEService.isPeerReachable` returns false or `canDeliverPromptly` indicates delays does the router invoke `attemptCourierDeposit` to route through Nostr.

### Can users disable the Nostr fallback to use only Bluetooth?

Yes. The gateway capability can be toggled via the `.gateway` bit. When disabled, the application restricts routing to BLE mesh only, though this limits delivery to peers within physical Bluetooth range.

### What is the role of the `onNostrCarrierPacket` closure in BLEService?

This closure acts as a bridge hook that allows `BLEService` to forward mesh payloads to `GatewayService` for Nostr publication. It enables the "carrier packet" pattern where BLE peers without internet access can still relay messages to Nostr through gateway-enabled nodes.