# How Private Messages Are Routed in BitChat: Bluetooth First, Nostr Fallback

> Discover how BitChat routes private messages using Bluetooth first and Nostr relays as a fallback. Ensures end-to-end encrypted communication locally and globally.

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

---

**BitChat routes private messages through a Bluetooth-first transport layer that automatically falls back to encrypted Nostr relays when peers are outside BLE range, ensuring end-to-end encryption across both local mesh and global internet delivery.**

The permissionlesstech/bitchat repository implements a resilient messaging protocol that prioritizes local radio privacy while maintaining global reachability. Understanding how private messages are routed in BitChat requires examining its dual-transport architecture, which orchestrates Bluetooth Low Energy (BLE) mesh networks with the Nostr protocol to deliver encrypted payloads regardless of connectivity conditions.

## Bluetooth-First Transport

BitChat establishes direct peer-to-peer connections using Bluetooth Low Energy when recipients are within radio range or reachable via multi-hop mesh relays. This path provides the fastest and most private delivery method because ciphertext never leaves the local radio environment.

### Noise XX Session Encryption

When a peer is reachable via BLE, BitChat establishes a **Noise XX handshake pattern** to create an encrypted session. The payload construction occurs in [`BLENoisePayloadFactory.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLENoisePayloadFactory.swift), which prepares the private message data for transmission over the encrypted channel. This ensures that even if BLE packets are intercepted by nearby devices, the content remains protected by the Noise protocol's cryptographic guarantees.

The transport layer checks availability through `TransportConfig.bluetoothEnabled` and peer reachability before attempting transmission.

## Nostr Fallback Protocol

When no Bluetooth path exists—either because the recipient is out of range or mesh connectivity is unavailable—the system transparently switches to **Nostr relays** for internet-based delivery.

### The Kind 1059 Private Envelope

BitChat defines a proprietary Nostr event type (**kind 1059**) for private messaging. In this fallback mode, the application creates a `PrivateEnvelope` addressed to the recipient's Nostr public key. The envelope encapsulates the original message payload in a format that standard Nostr relays can store and forward while maintaining BitChat's encryption standards.

### XChaCha20-Poly1305 Encryption Scheme

Before publishing to relays, the payload undergoes additional encryption using **XChaCha20-Poly1305** with a key derived from secp256k1 ECDH between sender and recipient keys. This creates a sealed envelope that only the intended recipient can decrypt, even though it traverses public Nostr relay infrastructure.

The `NostrTransport` class handles envelope creation and relay submission, publishing to built-in or user-configured relay endpoints.

## Smart Queueing During Connectivity Gaps

BitChat implements a **bounded outbound queue** for scenarios where neither Bluetooth nor Nostr connectivity is available. Outbound private messages are sealed with ChaCha20-Poly1305 and stored in the device's keychain-backed outbox for up to 24 hours.

Once either transport becomes available—whether the recipient enters Bluetooth range or the device re-establishes internet connectivity—the queue automatically drains and delivers pending messages through the appropriate transport layer.

## Transport Selection Implementation

The routing logic is centralized in [`UnifiedPeerService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/UnifiedPeerService.swift), which provides a unified API for message delivery while abstracting transport-specific details.

### Transport Selection Logic

When [`ChatViewModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatViewModel.swift) initiates a private message send, it delegates to the unified service:

```swift
// ChatViewModel.swift – public API used by UI
await chatViewModel.sendPrivateMessage(
    to: recipientPeerID,
    content: "Hello, secret world!"
)

```

Inside `UnifiedPeerService`, the transport selection occurs dynamically:

```swift
func send(_ payload: NoisePayload, to peerID: PeerID) async {
    if bleTransport.isReachable(peerID) {
        await bleTransport.send(payload, to: peerID)          // Bluetooth path
    } else {
        await nostrTransport.sendEncryptedEnvelope(payload, to: peerID) // Nostr fallback
    }
}

```

This method checks `TransportConfig.bluetoothEnabled` and peer reachability via `bleTransport.isReachable()` before falling back to `nostrTransport.sendEncryptedEnvelope()`.

### Inbound Message Normalization

Incoming messages follow a mirrored path. The [`NostrInboundPipeline.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrInboundPipeline.swift) receives encrypted envelopes from Nostr relays, decrypts them using the shared ECDH-derived keys, and injects the plaintext payloads into the same private-message handling pipeline used for BLE deliveries.

This architectural choice preserves a uniform private-message API throughout the application, ensuring that UI components in [`ChatViewModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatViewModel.swift) receive consistently formatted messages regardless of whether they arrived via Bluetooth mesh or Nostr fallback.

### Private Envelope Construction

When the Nostr path is selected, the envelope creation proceeds as follows:

```swift
let envelope = PrivateEnvelope(
    recipientPubKey: peer.nostrPubKey,
    encryptedContent: XChaCha20Poly1305.encrypt(
        plaintext: payload.data,
        key: derivedKeyFromECDH(peer.nostrPubKey)
    )
)
await nostrRelay.publish(event: envelope.asNostrEvent())

```

## Summary

BitChat's private message routing combines local mesh privacy with global internet reachability:

- **Bluetooth-first delivery** uses Noise XX encryption for direct peer-to-peer transmission when recipients are within BLE range
- **Nostr fallback** employs kind 1059 private envelopes with XChaCha20-Poly1305 encryption for internet-based relay delivery
- **`UnifiedPeerService`** dynamically selects transports based on `TransportConfig.bluetoothEnabled` and peer reachability checks
- **24-hour bounded queue** stores offline messages in keychain-encrypted storage, automatically draining when connectivity returns
- **Normalized inbound processing** via [`NostrInboundPipeline.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrInboundPipeline.swift) ensures consistent handling regardless of transport origin

## Frequently Asked Questions

### How does BitChat decide between Bluetooth and Nostr for private messages?

[`UnifiedPeerService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/UnifiedPeerService.swift) evaluates transport availability by checking `TransportConfig.bluetoothEnabled` and calling `bleTransport.isReachable(peerID)` for the specific recipient. If the peer is reachable via the BLE mesh, the message transmits through the encrypted Bluetooth channel; otherwise, the system automatically falls back to `NostrTransport` for internet relay delivery.

### What encryption protects messages when using Nostr fallback?

Nostr fallback messages use a dual-layer encryption scheme. First, BitChat derives a shared secret via secp256k1 ECDH between sender and recipient keys. This key encrypts the payload using XChaCha20-Poly1305, creating a sealed private envelope (kind 1059). Only the recipient's device can decrypt the envelope after retrieval from public Nostr relays.

### Where are pending messages stored when both Bluetooth and Nostr are unavailable?

BitChat maintains a bounded outbox in the device's secure keychain. Messages are sealed with ChaCha20-Poly1305 and stored for a maximum of 24 hours. The `UnifiedPeerService` monitors transport state and automatically drains this queue once either Bluetooth or Nostr connectivity becomes available.

### Does BitChat use different code paths for BLE and Nostr messages after delivery?

No. While delivery uses transport-specific implementations in [`BLENoisePayloadFactory.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLENoisePayloadFactory.swift) (for BLE) and `NostrTransport` (for Nostr), inbound messages converge in [`NostrInboundPipeline.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrInboundPipeline.swift). This pipeline decrypts Nostr envelopes and injects them into the same private-message handling path used for BLE deliveries, providing a uniform API to UI components in [`ChatViewModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatViewModel.swift).