# What Is the Sender Outbox in BitChat and How Does It Work?

> Discover the BitChat Sender Outbox, a secure queue for guaranteed message delivery. Learn how it ensures messages reach recipients even through restarts and network issues.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: how-to-guide
- Published: 2026-08-22

---

**The Sender Outbox in BitChat is a durable, per-peer encrypted queue that stores outgoing direct messages until the recipient acknowledges receipt, guaranteeing exactly-once delivery even across app restarts and network failures.**

BitChat is an open-source, peer-to-peer messaging application developed by permissionlesstech that prioritizes privacy and reliability. The **Sender Outbox** ensures that direct messages (DMs) are never lost when the recipient is offline or unreachable, persisting them to disk with strong encryption. This article examines the Swift implementation in [`MessageOutboxStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageOutboxStore.swift) and [`MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouter.swift) to explain how the outbox manages, secures, and flushes outbound traffic.

## Implementation Architecture

### MessageOutboxStore (Persistent Storage Layer)

The foundation of the Sender Outbox is `MessageOutboxStore`, located at [`bitchat/Services/Courier/MessageOutboxStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/Courier/MessageOutboxStore.swift). This class maintains a persistent dictionary of queued messages indexed by the recipient’s **peer-ID**.

Key implementation details:

- **Encryption at rest**: Each queued payload is encrypted with a symmetric key derived from the recipient’s fingerprint and a per-app secret stored in the iOS Keychain. This ensures that even if the device is compromised, the outbox remains unreadable without the Keychain material.
- **Atomic writes**: The store writes the entire queue to disk atomically, preventing corruption if the app terminates mid-write.
- **In-memory recovery**: On initialization, `load()` reads the persisted file and reconstructs the `[PeerID: [QueuedMessage]]` dictionary, allowing the app to resume sending operations immediately after relaunch.

### MessageRouter (Coordination Layer)

Located at [`bitchat/Services/Courier/MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/Courier/MessageRouter.swift), the router acts as the traffic controller between the UI and the outbox. It exposes high-level methods such as `sendMessage(_:to:)` and `flushOutbox(for:)`, while internally managing transport selection (BLE, TCP, Nostr, etc.) and retry logic.

The router enforces delivery policies:

- **TTL expiry**: Messages older than a configurable threshold are automatically dropped.
- **Attempt caps**: A maximum retry count prevents infinite loops against unreachable peers.
- **ACK-driven removal**: A message is only deleted from `MessageOutboxStore` after a valid acknowledgment (ACK) is received from the remote peer, ensuring exactly-once semantics.

### Keychain Integration

The outbox relies on a `Keychain` protocol implementation passed into `MessageOutboxStore`'s initializer. The store uses this to retrieve the master secret required to derive per-peer encryption keys. This separation of concerns keeps cryptographic material out of the queue management logic and leverages the Secure Enclave where available.

## Lifecycle of an Outgoing Message

Understanding the flow of a DM through the Sender Outbox clarifies how reliability is achieved:

1. **Enqueue**: When the user sends a DM, `MessageRouter` creates a `QueuedMessage` value, increments the revision counter, and calls `outboxStore.save([peerID: [queued]])`. The payload is encrypted and written to disk before the network request begins.
2. **Attempt**: The router selects a transport and transmits the encrypted blob. The `attemptCount` field on the queued entry is incremented and persisted, allowing crash recovery to respect retry limits.
3. **ACK processing**: Upon receiving an ACK packet from the peer, the router invokes `flushOutbox(for: peerID)`. This method queries the store for all acknowledged entries and atomically removes them, freeing disk space.
4. **Retry / Failure**: If the attempt cap or TTL is reached without an ACK, the router invokes the `onMessageDropped` callback (assigned by the UI layer) and deletes the entry from the store.
5. **App restart**: On cold start, `MessageOutboxStore.load()` restores pending entries. The router immediately re-queues them for delivery, preserving the original order per recipient.

## Practical Code Examples

### Enqueue a Direct Message

```swift
import Foundation

// Initialize the store with Keychain-backed encryption
let outboxStore = MessageOutboxStore(
    keychain: appKeychain,
    fileURL: FileManager.default
        .urls(for: .documentDirectory, in: .userDomainMask)[0]
        .appendingPathComponent("outbox.enc")
)

let router = MessageRouter(outboxStore: outboxStore)

// Queue a message; encryption and persistence happen synchronously
await router.sendMessage(
    DirectMessage(id: UUID(), text: "Hello, world!", timestamp: Date()),
    to: recipientPeerID
)

```

### Flush Acknowledged Messages

```swift
// Called from the transport layer when an ACK packet arrives
router.flushOutbox(for: confirmedPeerID)
// All queued items for this peer that have been ACK'd are removed from disk

```

### Restore Pending Messages on Launch

```swift
let outboxStore = MessageOutboxStore(keychain: appKeychain, fileURL: outboxURL)

// Recover state after app termination
let pending = outboxStore.load()  // Returns [PeerID: [QueuedMessage]]
router.restorePendingMessages(pending)

// Router now schedules background retries for any remaining entries

```

### Handle TTL or Attempt-Cap Drops

```swift
router.onMessageDropped = { peerID, messageID in
    // Surface failure to the user or log analytics
    print("Failed to deliver message \(messageID) to \(peerID) after max retries")
    // UI update omitted for brevity
}

```

## Summary

- The **Sender Outbox** is a per-peer, encrypted queue implemented in [`MessageOutboxStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageOutboxStore.swift) that persists outbound DMs until delivery is confirmed.
- **Exactly-once delivery** is achieved by removing messages only after receiving an ACK from the recipient, while TTL and attempt caps prevent resource exhaustion.
- **Encryption** uses symmetric keys derived from peer fingerprints and Keychain-stored secrets, protecting message content at rest.
- **Automatic recovery** after app restart is handled by `load()`, which restores the in-memory queue from disk so the `MessageRouter` can resume sending without user intervention.
- **Failure handling** is delegated via the `onMessageDropped` closure, allowing the UI to react to unreachable peers or expired messages.

## Frequently Asked Questions

### How does the Sender Outbox handle app restarts?

The `MessageOutboxStore.load()` method reads the persisted encrypted file from disk and reconstructs the `[PeerID: [QueuedMessage]]` dictionary. When the `MessageRouter` initializes, it calls `restorePendingMessages()` with this data, automatically re-scheduling any unsent messages for delivery. This ensures that force-quitting or crashing the app never loses pending outbound traffic.

### What triggers a message to be removed from the outbox?

A message is removed only when `flushOutbox(for:)` is called after the router receives a valid acknowledgment (ACK) from the remote peer. If the message exceeds its configured TTL or attempt count before an ACK arrives, the router invokes `onMessageDropped` and then deletes the entry. This dual-path removal guarantees that the user is notified of failures while successful deliveries are idempotent.

### How is the outbox encrypted to protect message privacy?

Each queued message is encrypted using a symmetric key derived from the recipient’s fingerprint and a master secret retrieved from the iOS Keychain. The `MessageOutboxStore` performs this encryption before writing to the file system, ensuring that the raw plaintext of DMs is never exposed in device backups or file system snapshots.

### Where are the reliability guarantees tested in the codebase?

Unit and integration tests in [`MessageOutboxStoreTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageOutboxStoreTests.swift) verify atomic persistence and recovery behavior, while [`MessageRouterTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouterTests.swift) covers TTL expiry, attempt-cap logic, and ACK-driven flushing. Additional architectural context regarding outbox durability during peer-ID rotation can be found in [`PEER-ID-ROTATION.md`](https://github.com/permissionlesstech/bitchat/blob/main/PEER-ID-ROTATION.md) at the repository root.