How Messages Are Routed and Delivered in the Bitchat Network: A Deep Dive into MessageRouter
Bitchat uses a tiered MessageRouter service that selects transports based on peer connectivity, falls back to opportunistic couriers for offline delivery, and bridges to internet relays when mesh paths fail—implementing a robust store-and-forward system with 24-hour TTL outbox persistence.
The bitchat network, developed by permissionlesstech, is a permissionless messaging protocol designed for resilience across intermittent connectivity. Unlike centralized messaging apps, bitchat's message routing and delivery mechanism must handle peers that come and go from the network constantly. The core orchestrator for this challenge is the MessageRouter service in Services/MessageRouter.swift.
Transport Selection Tiers in MessageRouter
When you send a private message, the MessageRouter evaluates available transports in three priority tiers. Each tier determines both how the message is sent and whether redundant courier deposits are needed.
Connected Transport with Secure Session
If the recipient has an active connection and an established Noise session, the message takes the fastest path. The router:
- Sends the payload over the secure channel
- Records the transmission in
secureTransmissionsfor potential retry after session resets
// Connected transport with Noise session
router.sendPrivate(
"Hey, meet me at 5 pm!",
to: recipientPeerID,
recipientNickname: "Alice",
messageID: UUID().uuidString
)
Connected Without Secure Session
When a peer is connected but lacks an authenticated Noise session, the router sends the payload and immediately initiates a courier deposit. This ensures message availability even if the direct connection drops during authentication.
Reachable Transport Only
For peers that are merely reachable—such as a Nostr relay showing the recipient's public key online—the message is queued for transmission. If prompt delivery isn't guaranteed, the router attempts a courier deposit as insurance.
No Reachable Transport
When no path exists, the message enters the outbox and is handed exclusively to couriers for deferred delivery.
Courier Handling and Opportunistic Routing
The courier system is bitchat's answer to intermittent connectivity. When direct delivery fails, the MessageRouter recruits other connected peers to physically carry encrypted envelopes across the mesh.
Courier Selection Process
The router queries the CourierDirectory—backed by the local favorites store—to locate the recipient's static Noise key. It then:
- Selects up to
maxCouriersPerMessageeligible couriers - Prioritizes mutual favorites for trust and availability
- Invokes
sendCourierMessageon eachMeshCourierTransportingtransport
Successful deposits are recorded via recordCourierDeposit and exposed through the onMessageCarried callback:
router.onMessageCarried = { msgID, peerID in
print("📦 Message \(msgID) handed to a courier for \(peerID)")
}
Bridge Deposit to Internet Relays
As a final fallback to the mesh, bitchat can deposit sealed envelopes onto internet relays. The bridgeCourierDeposit closure executes after courier attempts, with its completion flag indicating whether at least one relay accepted the envelope. This bridges the local mesh to the broader Nostr relay network when peer-to-peer paths fail entirely.
Outbox Management and Persistence
Every outgoing private message enters an in-memory outbox ([PeerID: [QueuedMessage]]) with optional persistence through MessageOutboxStore. The outbox enforces:
| Constraint | Value | Purpose |
|---|---|---|
messageTTLSeconds |
24 hours | Maximum retention before cleanup |
maxMessagesPerPeer |
(configurable) | Per-peer queue depth limit |
Outbox Lifecycle Operations
The MessageRouter runs three critical maintenance routines:
flushOutbox(for:)– Attempts delivery when a peer becomes reachableretrySecurePrivateMessagesAfterAuthentication()– Retries messages after Noise session establishmentcleanupExpiredMessages()– Removes items exceeding 24-hour TTL
When delivery or read receipts arrive, markDelivered removes the retained copy, releases courier slots, and persists the state change.
// Flush when connectivity changes
router.flushOutbox(for: recipientPeerID)
// Handle failures
router.onMessageDropped = { msgID, peerID in
print("❌ Message \(msgID) to \(peerID) failed")
}
Read Receipt Routing
Read receipts follow the same tiered routing logic as messages. The sendReadReceipt method attempts delivery through any reachable transport. If none are available, the receipt remains in-flight for later retry—ensuring eventual acknowledgment without blocking the UI.
Key Source Files for Message Routing
Understanding bitchat's routing implementation requires familiarity with these files:
Services/MessageRouter.swift– Core routing logic, outbox management, courier coordination, and bridge fallbackslocalPackages/BitFoundation/Sources/BitFoundation/BitchatMessage.swift– Wire format including sender, payload, and encryption flagslocalPackages/BitFoundation/Sources/BitFoundation/BinaryProtocol.swift– Low-level packet encoding with optional routing hop metadatadocs/SOURCE_ROUTING.md– Design documentation for routing decisions and fallback strategiesdocs/ARCHITECTURE_V2.md– Overview of the store-and-forward architecture
Summary
- Tiered transport selection in
MessageRouter.swiftprioritizes secure direct connections, falls back to unsecured direct links with courier backup, and finally delegates to purely opportunistic routing - Courier deposit recruits mutual favorites to carry encrypted envelopes when recipients are unreachable, with
maxCouriersPerMessagelimiting redundancy - Bridge fallback deposits messages onto Nostr relays via
bridgeCourierDepositwhen mesh couriers fail - 24-hour TTL outbox with
flushOutbox,cleanupExpiredMessages, andmarkDeliveredensures eventual delivery without unbounded growth - Deduplication by message ID on the receiver side prevents duplicate processing across multiple delivery paths
Frequently Asked Questions
What happens if the recipient is completely offline when I send a message?
The message is stored in the outbox with a 24-hour TTL and handed to available couriers. If couriers cannot reach the recipient, the bridgeCourierDeposit closure attempts to deposit the sealed envelope on Nostr relays. The flushOutbox method retries delivery automatically when the recipient becomes reachable.
How does bitchat prevent message duplication across multiple couriers?
Recipients deduplicate by message ID on arrival. While multiple couriers may carry the same envelope, the receiver processes only the first successful delivery and discards subsequent copies with identical IDs.
What determines which peers become couriers for my messages?
The CourierDirectory selects couriers from favorites-boosted candidates, preferring mutual favorites for trust and availability. The router respects maxCouriersPerMessage to limit network overhead while maintaining reliable redundancy.
Can I configure how long messages remain in the outbox?
The TTL is fixed at 24 hours via messageTTLSeconds in the current implementation. However, maxMessagesPerPeer is configurable to control per-recipient queue depth and storage pressure.
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 →