# How BitChat's Store-and-Forward Courier System Enables Offline Messaging

> Discover BitChat's store-and-forward courier system. Learn how it combines BLE mesh and Nostr for reliable offline messaging to any recipient.

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

---

**BitChat delivers messages to offline or out-of-range recipients through a multi-layer store-and-forward courier stack that combines local BLE mesh transport with Nostr bridge fallback.**

The `permissionlesstech/bitchat` repository implements a robust **store-and-forward courier system** designed for mesh networks where peers are frequently disconnected. Rather than requiring always-on connectivity, BitChat lets messages physically travel through intermediate devices or bridge through the Internet when necessary.

## Core Components of the Courier Stack

Three abstractions work together to move sealed messages across intermittent networks:

| Component | Purpose |
|-----------|---------|
| **`CourierEnvelope`** | The atomic transport unit containing ciphertext, expiry, recipient tag, and copy budget |
| **`MessageRouter`** with `CourierDirectory` | Local outbox persistence and matching of envelopes to nearby couriers |
| **`BridgeCourierService`** | Internet fallback that publishes encrypted envelopes as Nostr events |

### CourierEnvelope: The Encrypted Payload

Every message travels as a **`CourierEnvelope`** — a compact TLV structure defined in [[`localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift)](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift). The envelope contains:

- **Recipient tag**: A deterministic identifier derived from the recipient's static Noise key and current epoch day, computed via `CourierEnvelope.recipientTag(noiseStaticKey:epochDay:)`
- **Ciphertext**: The message encrypted to the recipient's static Noise key
- **Expiry timestamp**: Maximum lifetime (default ~6 hours via `CourierEnvelope.maxLifetimeSeconds`)
- **Copy budget** (`copies`): How many redundant couriers may carry this envelope (default `CourierEnvelope.maxCopies`)

The envelope reveals no routing metadata — only the recipient can decrypt, and the tag alone cannot identify the recipient without the corresponding key.

### MessageRouter: Local Store-and-Forward

When direct delivery fails, [[`bitchat/Services/MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MessageRouter.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MessageRouter.swift) handles persistence and courier discovery:

1. **Outbox storage**: Envelopes are written to the **Message Outbox Store** (verified in [[`MessageOutboxStoreTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageOutboxStoreTests.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/MessageOutboxStoreTests.swift))
2. **Courier directory**: Nearby devices broadcast favorite courier keys — essentially public static Noise keys they are willing to transport messages for
3. **Deposit matching**: The router compares the envelope's `recipientTag` against advertised courier keys; on match, it marks the outbox entry as *deposited* and hands the envelope to the carrier

The directory construction logic is exercised in [[`MessageRouterTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouterTests.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/MessageRouterTests.swift#L458-L461) around lines 458-461.

### BridgeCourierService: Internet Fallback

When local couriers remain unavailable, [[`bitchat/Services/BridgeCourierService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BridgeCourierService.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BridgeCourierService.swift) provides the **bridge path**:

- Creates **drop events** (`NostrEvent` structs) containing one or more `CourierEnvelope` instances
- Signs the event and publishes to a Nostr relay
- Recipients periodically fetch drops, verify the envelope tag matches their key, and open the ciphertext

The bridge path is documented in the [WHITEPAPER.md store-and-forward section](https://github.com/permissionlesstech/bitchat/blob/main/WHITEPAPER.md#store-and-forward). Expiration logic is validated in [[`BridgeCourierServiceTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BridgeCourierServiceTests.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/BridgeCourierServiceTests.swift#L319-L321) at lines 319-321.

## Store-and-Forward Workflow Step by Step

### 1. Sealing and Outbox Persistence

```swift
// Create recipient tag from static Noise key and current epoch day
let recipientKey = bobNoiseStaticKey  // 32-byte X25519 public key
let ciphertext = try encrypt(message, with: recipientKey)

let envelope = CourierEnvelope(
    recipientTag: CourierEnvelope.recipientTag(
        noiseStaticKey: recipientKey,
        epochDay: CourierEnvelope.epochDay(for: Date())
    ),
    expiry: Date().addingTimeInterval(CourierEnvelope.maxLifetimeSeconds),
    ciphertext: ciphertext,
    copies: 3,           // spray-and-wait redundancy
    prekeyID: nil        // optional for future extensions
)

// Persist to outbox and attempt delivery
await messageRouter.sendPrivate(
    messageID: "msg-123",
    recipient: bobPeerID,
    envelope: envelope
)

```

The `sendPrivate(messageID:recipient:envelope:)` method in [`MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouter.swift) either transmits immediately via an active Noise session or queues for courier pickup.

### 2. Courier Discovery and Deposit

Nearby peers advertising compatible courier keys trigger the deposit mechanism. The router:

- Scans the `CourierDirectory` for `recipientTag` matches
- Transfers the envelope to the courier device
- Decrements the copy budget and marks outbox status

Physical transport metrics are recorded via [[`StoreAndForwardMetrics.swift`](https://github.com/permissionlesstech/bitchat/blob/main/StoreAndForwardMetrics.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/Courier/StoreAndForwardMetrics.swift), tracking drops sent and copies delivered without revealing content or participants.

### 3. Bridge Drop Creation (Fallback)

```swift
// If no courier appears before deadline, bridge to Nostr
let bridgeService = BridgeCourierService(
    now: Date.now,
    dedupStore: DedupStore()
)

let dropEvent = try bridgeService.makeDropEvent(for: envelope)
try await nostrClient.publish(event: dropEvent)

```

The `makeDropEvent(for:)` method batches envelopes, signs the Nostr event, and sends to configured relays. Recipients use standard Nostr filters to fetch drops matching their tags.

### 4. Delivery and Cleanup

Upon successful decryption:

- The recipient's device opens the envelope using its static Noise private key
- The sender's outbox deletes the corresponding entry
- Held copies on couriers expire silently after `maxLifetimeSeconds`

## Privacy and Reliability Guarantees

BitChat's store-and-forward courier system provides three key properties:

- **Spray-and-wait reliability**: Multiple redundant copies (`copies` > 1) ensure at least one carrier succeeds before expiry
- **Sealed privacy**: Envelopes are opaque to couriers; only the recipient's static key can decrypt
- **Bounded ephemerality**: Hard expiration prevents indefinite storage and metadata accumulation

## Summary

- **CourierEnvelope** is the encrypted, tagged, expiring unit of transport in [[`CourierEnvelope.swift`](https://github.com/permissionlesstech/bitchat/blob/main/CourierEnvelope.swift)](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift)
- **MessageRouter** coordinates outbox persistence and local courier matching in [[`MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouter.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MessageRouter.swift)
- **BridgeCourierService** provides Internet fallback via Nostr in [[`BridgeCourierService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BridgeCourierService.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BridgeCourierService.swift)
- The complete workflow involves sealing, outbox storage, courier discovery, physical transport, optional bridge fallback, and expiration-based cleanup

## Frequently Asked Questions

### What happens if no courier is available before the envelope expires?

The `MessageRouter` automatically triggers the bridge path. [`BridgeCourierService`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BridgeCourierService.swift) creates a signed Nostr event containing the envelope and publishes it to relays. The recipient can then fetch the drop using standard Nostr subscription filters. This ensures delivery even in partitioned mesh networks.

### How does BitChat prevent couriers from reading message contents?

Envelopes are **sealed** with the recipient's static Noise key before ever entering the courier system. The `CourierEnvelope` only exposes a truncated recipient tag (derived from the key and epoch day) and opaque ciphertext. Couriers match tags to advertised keys without possessing the private key required for decryption. This design provides **end-to-end encryption** independent of transport path.

### Why does the recipient tag include an epoch day component?

The epoch day (days since Unix epoch) creates **time-bounded identifiers** that rotate daily. This prevents long-term correlation of recipient tags across days, limiting the metadata exposure that couriers or Nostr relays could accumulate. A recipient's tag today differs from yesterday's, yet remains deterministic for legitimate senders who know the static key and current date.

### What limits how many copies a single envelope can generate?

The `copies` field in `CourierEnvelope` enforces a **copy budget** — typically defaulting to `CourierEnvelope.maxCopies`. Each handoff to a courier decrements this counter. When `copies` reaches zero, no further deposits occur. This spray-and-wait approach bounds network overhead while providing statistical reliability through redundant paths.