How BitChat's Intelligent Transport Selection Routes Private Messages: BLE Mesh, Nostr, and Courier Fallbacks
BitChat routes every private message through a priority-ranked decision engine that prefers secure BLE mesh connections, falls back to reachable peers, and automatically delegates to trusted couriers or Nostr bridges when direct delivery fails.
BitChat's peer-to-peer chat protocol eliminates centralized servers by dynamically selecting the optimal transport layer for each message. According to the permissionlesstech/bitchat source code, this intelligent transport selection is implemented in MessageRouter.swift, where a four-step evaluation cascade balances latency, security, and delivery guarantees.
How the MessageRouter Evaluates Transport Options
The core routing logic resides in MessageRouter.sendPrivate(_:to:recipientNickname:messageID:) at lines 92-162. The router maintains an outbox of queued messages and queries each registered Transport implementation—currently BLE mesh and Nostr bridge—for connectivity state.
Two helper methods provide the foundation for transport selection:
private func reachableTransport(for peerID: PeerID) -> Transport? {
transports.first { $0.isPeerReachable(peerID) }
}
private func connectedTransport(for peerID: PeerID) -> Transport? {
transports.first { $0.isPeerConnected(peerID) }
}
These queries determine whether the recipient appears in peer advertisements (isPeerReachable) or has an established session (isPeerConnected). The router then applies its four-tier priority system.
The Four-Tier Transport Selection Algorithm
| Priority | Condition | Transport Used | Courier Backup? |
|---|---|---|---|
| 1 | Connected and secure session exists | Secure BLE mesh (Noise protocol) | No |
| 2 | Connected but no secure session | Connected mesh (unauthenticated) | Yes |
| 3 | Only reachable (advertised, not connected) | Reachable mesh | Yes, if delivery not prompt |
| 4 | No transport available | Queued only (outbox) | Yes, immediate |
Each tier modifies message state and courier behavior differently.
Tier 1: Secure Direct Delivery
When connectedTransport(for:) returns a transport where canDeliverSecurely(to:) is true—meaning a completed Noise protocol handshake exists—the router takes the fastest path:
if let transport = connectedTransport(for: peerID),
transport.canDeliverSecurely(to: peerID) {
enqueue(message, for: peerID)
secureTransmissions.insert(PeerMessageKey(peerID: peerID, messageID: messageID))
transport.sendPrivateMessage(content, …)
return
}
The message is marked as secureTransmission, enabling optimized retry logic later. No courier involvement occurs—privacy is preserved through direct encryption.
Tier 2: Connected but Unauthenticated
If the peer is connected without a Noise session, the router still uses the mesh but adds courier redundancy:
if let transport = connectedTransport(for: peerID) {
enqueue(message, for: peerID)
secureTransmissions.remove(PeerMessageKey(peerID: peerID, messageID: messageID))
transport.sendPrivateMessage(content, …)
attemptCourierDeposit(messageID: messageID, for: peerID)
return
}
The message is sent immediately, but attemptCourierDeposit seals and hands a copy to eligible couriers as insurance against delivery failure.
Tier 3: Reachable-Only Peers
For peers visible in advertisements but without active connections:
if let transport = reachableTransport(for: peerID) {
enqueue(message, for: peerID)
transport.sendPrivateMessage(content, …)
if !transport.canDeliverPromptly(to: peerID) {
attemptCourierDeposit(messageID: messageID, for: peerID)
}
}
The canDeliverPromptly check distinguishes between transports that will likely succeed quickly versus those experiencing delays. Nostr bridges returning false here trigger immediate courier delegation.
Tier 4: Complete Transport Failure
When no transport reports reachability:
} else {
var unsent = message; unsent.sendAttempts = 0
enqueue(unsent, for: peerID)
attemptCourierDeposit(messageID: messageID, for: peerID)
}
The message enters the outbox with zero send attempts and relies entirely on courier or bridge infrastructure.
Courier Selection and Bridge Backup
The attemptCourierDeposit method at lines 66-78 implements BitChat's store-and-forward resilience. It operates in two phases:
- Bridge deposit: Request Nostr relay storage via
requestBridgeCourierDeposit - Mesh courier deposit: Select trusted peers to physically carry the message
private func attemptCourierDeposit(messageID: String, for peerID: PeerID) {
guard let recipientKey = courierDirectory.noiseKey(peerID),
let entry = queuedMessage(messageID, for: peerID) else { return }
// Phase 1: Internet bridge
requestBridgeCourierDeposit(entry, for: peerID, recipientKey: recipientKey)
// Phase 2: Mesh couriers
let remainingSlots = Self.maxCouriersPerMessage - entry.depositedCourierKeys.count
guard remainingSlots > 0 else { return }
for transport in transports {
guard let courierTransport = transport as? MeshCourierTransporting else { continue }
let couriers = eligibleCouriers(on: transport,
recipientKey: recipientKey,
excluding: entry.depositedCourierKeys,
limit: remainingSlots)
// Send to selected couriers...
}
}
Courier Eligibility Scoring
The eligibleCouriers method (lines 40-60) ranks candidates by:
- Mutual favorite status: Peers marked as favorites in both directions receive highest priority
- Verification state: Cryptographically verified identities outrank unverified strangers
- Connection health: Currently connected couriers preferred over cached snapshots
- Deduplication: Already-used couriers excluded via
depositedCourierKeys
Results are sorted favorites-first and capped at maxCouriersPerMessage to limit network overhead.
Outbox Persistence and Retry Logic
BitChat's transport selection includes durable delivery guarantees through MessageOutboxStore.swift. Key mechanisms:
- FIFO per-peer queues: Each recipient maintains up to 100 pending messages
- Secure transmission tracking: The
secureTransmissionsset limits retries for already-encrypted messages - Periodic sweeps: Background tasks retry bridge deposits and purge expired entries (30-day default TTL)
When a courier connects or a transport state changes, courierBecameAvailable triggers re-evaluation of pending deposits without user intervention.
Integrating Transport Selection in Applications
Developers interact with the routing layer through the MessageRouter public interface:
let router = MessageRouter(transports: [bleTransport, nostrTransport])
// Send with automatic transport selection
router.sendPrivate(
"Meet at coordinates 40.7128, -74.0060",
to: recipientPeerID,
recipientNickname: "field-agent-7",
messageID: UUID().uuidString
)
// React to infrastructure changes
router.courierBecameAvailable(newCourierPeerID)
// Monitor delivery status
bleTransport.eventDelegate = myDelegate
The TransportEvent protocol provides asynchronous callbacks for peerConnected, peerSnapshotsUpdated, and messageDeliveryStatusUpdated, enabling UI synchronization without polling.
Summary
- Four-tier cascade: BitChat evaluates secure connected, connected, reachable, and unavailable states in strict priority order
- Automatic courier delegation: Every non-secure path triggers sealed message deposits to trusted peers or Nostr bridges
- Privacy-preserving redundancy: Courier messages use recipient Noise keys for encryption—intermediate peers cannot read content
- Durable outbox: Per-peer queues with retry logic ensure eventual delivery across intermittent connectivity
Frequently Asked Questions
How does BitChat decide between BLE mesh and Nostr for a message?
The router queries all registered transports equally through connectedTransport(for:) and reachableTransport(for:). The first transport returning true for connectivity checks wins priority, with secure BLE connections preferred over Nostr bridges when both are available. If neither reports prompt delivery capability, both receive courier deposits.
What makes a peer eligible to carry messages for others?
Eligibility requires: (1) a known Noise public key in courierDirectory, (2) either mutual favorite status or cryptographic verification, and (3) current connectivity or recent presence in peer snapshots. Favorites are always prioritized over verified strangers, and each message caps courier count via maxCouriersPerMessage.
Can messages be read by courier peers?
No. The attemptCourierDeposit method seals messages using the recipient's static Noise key from courierDirectory.noiseKey. Couriers receive encrypted envelopes they cannot decrypt—only the intended recipient possesses the corresponding private key.
What happens if all selected couriers fail to deliver?
The outbox entry persists with incremented sendAttempts. Periodic sweeps retry bridge deposits, and the router invokes eligibleCouriers again when new peers connect. Messages expire after the configured TTL (default 30 days) and are removed by cleanupExpiredMessages.
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 →