How BitChat Handles Message Routing Fallback from Bluetooth to Nostr
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.
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, the sendPrivate(_:to:recipientNickname:messageID:) method implements a three-tier selection process:
- Connected Transport: Check for an active secure session via
connectedTransport(for:)andcanDeliverSecurely(to:) - Reachable Transport: If no active session exists, verify BLE mesh availability using
reachableTransport(for:)andBLEService.isPeerReachable - 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
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 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.
// 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, the bridge closure wires directly to GatewayService.publishToRelays:
// 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 handles the protocol translation, creating signed Nostr events that carry the mesh payload:
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.swiftthat 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
onNostrCarrierPacketclosure wired inChatViewModelBootstrapper.swift - GatewayService handles protocol translation, signing mesh payloads as Nostr events with geohash-based identities
- Automatic retry logic via
retryBridgeCourierDepositsensures 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →