# How BitChat Handles Offline Private Messages: Store-and-Forward System Explained

> Discover how BitChat ensures offline private message delivery through its robust store-and-forward system. Learn about local storage, peer couriers, and internet bridges for seamless communication.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-08-21

---

**BitChat guarantees delivery of offline private messages using a store-and-forward pipeline that retains encrypted copies locally, hands sealed envelopes to trusted courier peers, and falls back to internet bridges when mesh delivery fails.**

BitChat's peer-to-peer architecture requires robust handling of **offline private messages** when recipients are unreachable via direct connections. The `permissionlesstech/bitchat` repository implements a resilient store-and-forward system built around three core components: a single-writer conversation store, an intelligent message router with persistent outbox management, and cryptographically sealed courier envelopes that opaque payloads for physical transport.

## Core Components of the Store-and-Forward Pipeline

The offline message system relies on three architectural layers working in concert to ensure eventual delivery without sacrificing privacy.

### ConversationStore as the Source of Truth

The `ConversationStore` serves as the single-writer source of truth for all chat history. When a user opens a private chat, the `PrivateChatManager` mirrors the store's selection via `selectedPeer`, and all incoming or outgoing messages append to this canonical history (`ConversationStore.append`). This design ensures that even if the store-and-forward layer retries multiple times, the conversation state remains consistent and deduplicated at the storage layer.

### MessageRouter and Persistent Outbox Management

The `MessageRouter` retains a copy of every outgoing private message and determines the optimal delivery path. Located in [`bitchat/Services/MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MessageRouter.swift), the router implements the critical `sendPrivate(_:to:recipientNickname:messageID:)` method.

When invoked, the router immediately creates a `QueuedMessage` and enqueues it:

```swift
func sendPrivate(_ content: String, to peerID: PeerID, …) {
    let message = QueuedMessage(content: content, …)
    enqueue(message, for: peerID)               // Retain copy regardless of delivery success
    // …transport selection logic…
    attemptCourierDeposit(messageID: message.messageID, for: peerID)
}

```

The outbox persists to disk via `outboxStore?.save(outbox)`, ensuring that **offline private messages** survive application restarts and device reboots.

### CourierEnvelope for Opaque Transport

The `CourierEnvelope` (defined in [`localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift)) creates an opaque container for physical couriers or internet bridges. Each envelope derives a unique **recipient tag** from the recipient's static Noise key:

```swift
CourierEnvelope.recipientTag(noiseStaticKey:epochDay:)

```

This tag allows couriers to route envelopes without decrypting payloads, while the envelope remains sealed with the sender's private key. The payload remains opaque to all intermediate hops, preserving end-to-end encryption even when messages traverse untrusted peer devices.

## How Offline Message Delivery Works

When the recipient is offline, BitChat transitions from immediate delivery attempts to a store-and-forward strategy leveraging trusted couriers and optional bridge relays.

### The Sending Process

The router follows a strict retention policy for every private message:

1. **Creates a queued copy** containing content, nickname, ID, and timestamp
2. **Attempts immediate delivery** via available transports (prioritizing connected and secure channels)
3. **Always retains the copy** in the outbox via `enqueue(message, for: peerID)`, regardless of immediate delivery success

This enqueue-then-attempt pattern guarantees that no message is lost due to transient transport failures.

### Courier Deposit When Recipients Are Offline

If no transport can deliver promptly (`!transport.canDeliverPromptly(to:)`), the router initiates **courier deposit** in [`bitchat/Services/MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MessageRouter.swift):

1. **Key Lookup**: Retrieves the recipient's static Noise key via `courierDirectory.noiseKey(peerID)` (offline favorites expose the full 64-hex key)
2. **Courier Selection**: Selects up to `maxCouriersPerMessage` (3) trusted couriers from mutual favorites using `eligibleCouriers`
3. **Envelope Distribution**: Calls `sendCourierMessage` for each selected courier, handing the sealed `CourierEnvelope` to mesh transports

The router records deposited courier keys via `recordCourierDeposit` to prevent duplicate deliveries to the same courier:

```swift
private func attemptCourierDeposit(messageID: String, for peerID: PeerID) {
    guard let recipientKey = courierDirectory.noiseKey(peerID),
          let entry = queuedMessage(messageID, for: peerID) else { return }
    
    // Bridge deposit (optional fallback)
    requestBridgeCourierDeposit(entry, for: peerID, recipientKey: recipientKey)
    
    // Hand to up to 3 physical couriers
    for transport in transports where transport is MeshCourierTransporting {
        let couriers = eligibleCouriers(on: transport,
                                        recipientKey: recipientKey,
                                        excluding: entry.depositedCourierKeys,
                                        limit: remainingSlots)
        transport.sendCourierMessage(entry.content,
                                     messageID: messageID,
                                     recipientNoiseKey: recipientKey,
                                     via: couriers.map(\.peerID))
    }
}

```

Physical couriers store these envelopes locally and carry them until encountering the offline peer via BLE mesh. When the recipient reconnects, their device decrypts the envelope and transmits an acknowledgment back through the mesh, triggering cleanup on the sender's device.

### Bridge Backup via Internet Relay

If configured, the router simultaneously calls `bridgeCourierDeposit` (a closure injected at bootstrap) to post the sealed envelope to a Nostr relay. This provides a secondary delivery path that functions independently of physical proximity or mesh connectivity. The bridge receives the identical opaque payload that physical couriers handle, maintaining cryptographic consistency across transport methods.

## Message Retention, TTL, and Cleanup

BitChat implements strict resource limits to prevent unbounded storage growth while ensuring adequate time for eventual delivery.

**Time-to-Live (TTL)**: Outbox entries expire after 24 hours (`messageTTLSeconds = 24 × 60 × 60`).

**Storage Cap**: Each peer's outbox is limited to 100 messages (`maxMessagesPerPeer`). When exceeded, the oldest message is evicted using an `evict oldest message` policy.

**Acknowledgment Handling**: Upon receiving delivery or read receipts, `markDelivered` removes the message from the outbox and persists a tombstone via `outboxStore?.recordRemoval`, preventing replay attacks while maintaining audit trails.

These mechanisms ensure that stale **offline private messages** do not consume device storage indefinitely, while active messages have sufficient time to reach recipients across intermittent connectivity patterns.

## Handling Identity Changes and Message Consolidation

Peers may appear under different identifiers (temporary Nostr IDs versus stable Noise keys). The `PrivateChatManager.consolidateMessages` method (located in [`bitchat/Services/PrivateChatManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/PrivateChatManager.swift)) resolves this by:

1. Detecting when a peer reconnects with a new ID but matching Noise key
2. Pulling retained messages from the old identifier's outbox
3. Updating the `senderPeerID` field to the new canonical ID
4. Appending messages to the new conversation while preserving unread status

This consolidation ensures that **offline private messages** sent to temporary identifiers eventually arrive in the correct conversation thread when the recipient stabilizes their network identity.

## Summary

- **BitChat retains every private message** in a persistent outbox via `MessageRouter.enqueue`, ensuring survival across app restarts and device reboots.
- **Courier envelopes** use recipient tags derived from Noise static keys, allowing opaque routing through untrusted intermediaries without decrypting payloads.
- **Physical couriers** receive sealed envelopes when immediate delivery fails, storing and forwarding messages via BLE mesh until encountering the offline recipient.
- **Internet bridges** provide parallel delivery paths through Nostr relays, functioning independently of mesh topology.
- **Automatic cleanup** occurs through 24-hour TTL expiration, 100-message per-peer caps, and acknowledgment-based tombstoning when recipients receive messages.

## Frequently Asked Questions

### How does BitChat ensure offline private messages aren't lost when the app closes?

The `MessageRouter` immediately persists every queued message to disk via `outboxStore?.save(outbox)` before attempting delivery. This write-ahead pattern ensures that **offline private messages** survive application termination, device reboots, and power cycles. When the app restarts, it reloads the outbox and resumes delivery attempts for any unacknowledged messages.

### Can couriers read the private messages they carry?

No. Couriers handle `CourierEnvelope` containers that remain cryptographically sealed. The envelope derives a routing tag from the recipient's Noise static key for addressing purposes, but the payload contents encrypt with the recipient's public key. Only the intended recipient possesses the private key necessary to decrypt the message contents, maintaining end-to-end confidentiality even when stored on intermediary devices.

### What happens if an offline recipient never comes back online?

Messages expire automatically after 24 hours (`messageTTLSeconds = 86,400`). Additionally, each peer's outbox maintains a hard limit of 100 messages (`maxMessagesPerPeer`), evicting the oldest entries when full. If the recipient remains offline beyond the TTL window, the sender's device permanently deletes the queued copy after recording a removal tombstone, freeing storage resources.

### How does BitChat prevent duplicate delivery of the same offline message?

The `MessageRouter` tracks deposited courier keys within each message entry (`entry.depositedCourierKeys`). Before handing an envelope to a courier, the router checks this set via `recordCourierDeposit` and excludes already-used couriers from subsequent `eligibleCouriers` queries. Once the recipient acknowledges receipt through any path (courier or bridge), `markDelivered` removes the message from the outbox entirely, preventing any future delivery attempts.