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 secureTransmissions for 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 maxCouriersPerMessage eligible couriers
  • Prioritizes mutual favorites for trust and availability
  • Invokes sendCourierMessage on each MeshCourierTransporting transport

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:

  1. flushOutbox(for:) – Attempts delivery when a peer becomes reachable
  2. retrySecurePrivateMessagesAfterAuthentication() – Retries messages after Noise session establishment
  3. cleanupExpiredMessages() – 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:

Summary

  • Tiered transport selection in MessageRouter.swift prioritizes 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 maxCouriersPerMessage limiting redundancy
  • Bridge fallback deposits messages onto Nostr relays via bridgeCourierDeposit when mesh couriers fail
  • 24-hour TTL outbox with flushOutbox, cleanupExpiredMessages, and markDelivered ensures 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →