# How Couriers in BitChat Deliver Offline Messages: Store-and-Forward Explained

> Discover how BitChat Couriers deliver offline messages using store-and-forward in Bluetooth mesh networks. Learn about this secure and decentralized communication method.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-22

---

**TLDR:** BitChat uses **Couriers** — trusted peers in a Bluetooth mesh network — to carry sealed, encrypted messages to offline recipients via a store-and-forward mechanism, with optional internet fallback through Nostr bridges.

BitChat (from `permissionlesstech/bitchat`) is a privacy-focused messenger that relies on local-only mesh networking. When a recipient is offline or unreachable, the sender doesn't just queue the message locally — it hands a sealed **`CourierEnvelope`** to up to three trusted **Couriers**, who physically or virtually carry it until the recipient comes online. This design powers offline resilience while keeping content fully encrypted. In this article, we'll break down how couriers function in BitChat for message delivery, referencing the actual source code.

## The Courier Architecture in BitChat

Couriers are not special server-side nodes. They are any connected, trusted peer in your mesh network — a friend, a mutual favorite, or a verified contact — that agrees to carry ephemeral payloads. The system implements a **spray-and-wait** strategy: the sender sprays copies of the envelope to several couriers, and at least one of them is expected to eventually deliver it.

The entire flow is orchestrated by three central building blocks:

- **`CourierEnvelope`** — the sealed message container with a rotating tag and an expiry.
- **`MessageRouter`** — picks couriers, retries deposits, and enforces budgets.
- **`BridgeCourierService`** — the internet fallback that publishes envelopes as Nostr drops.

Best of all, every piece is open source. You can inspect the exact logic in the repository's `localPackages` and `bitchat/Services` folders.

## Step 1: The Courier Envelope — Keeping the Message Sealed

A courier never sees the message content. The sender encrypt the plaintext into a **`CourierEnvelope`** before any routing decisions are made.

### Structure of a `CourierEnvelope`

According to the code in [`CourierEnvelope.swift`](https://github.com/permissionlesstech/bitchat/blob/main/CourierEnvelope.swift), the envelope includes:

- **`recipientTag`** — a rotating tag derived from the recipient's Noise static key and the current UTC day. This makes it impossible for any courier to recognize who the message is for, except the intended peer.
- **`expiry`** — a Unix timestamp in milliseconds after which the envelope is discarded. The source enforces a maximum of 24 hours via `CourierEnvelope.maxLifetimeSeconds`.
- **`ciphertext`** — the encrypted payload, opaque to the courier.
- **`copies`** — a copy budget that limits how many distinct couriers may hold a copy. This mitigates spam and amplification attacks.
- **`prekeyID`** — optional identifier for forward‑secret (FS) envelopes, used when ratcheting is needed.

The envelope is TLV-encoded (Type-Length-Value), making it compact over the wire.

```swift
let recipientTag = CourierEnvelope.recipientTag(
    noiseStaticKey: recipientNoiseKey,
    epochDay: CourierEnvelope.epochDay(for: Date())
)

let envelope = CourierEnvelope(
    recipientTag: recipientTag,
    expiry: UInt64(Date().addingTimeInterval(24*60*60).timeIntervalSince1970 * 1000),
    ciphertext: encryptedPayload,
    copies: 3                        // allow up to 3 spray copies
)

```

## Step 2: Selecting Eligible Couriers and Depositing the Envelope

When a direct send fails (no transport to the recipient), the `MessageRouter` takes over. In [`MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouter.swift), the `eligibleCouriers` method builds a list of potential carriers by filtering connected peers:

- Peers that are **verified** (mutual TLS or trust knowledge)  
- Peers that are in the **mutual‑favorite** set

The available set is then returned as `eligibleCouriers`. The router then picks up to `maxCouriersPerMessage` (currently **3**) and hands the envelope to them via the Bluetooth mesh.

The actual deposit happens through `MeshCourierTransporting.sendCourierMessage`, and a successful delivery record is stored locally via `recordCourierDeposit`. This prevents over‑spraying to the same courier later.

Internally, the flow looks like this (simplified):

```swift
let candidates = router.eligibleCouriers(
    on: meshTransport,
    recipientKey: recipientNoiseKey,
    excluding: [],                  // no prior deposits
    limit: MessageRouter.maxCouriersPerMessage
)

meshTransport.sendCourierMessage(
    envelope.ciphertext,            // opaque payload
    messageID: messageID,
    recipientNoiseKey: recipientNoiseKey,
    via: candidates.map(\.peerID)
)

```

Note that `sendPrivate` in the router encapsulates these steps, so the caller just provides the message content, recipient ID, and a message ID.

## Step 3: Retry & Spread – How Couriers Ensure Delivery

The intelligence of courier delivery lies in BitChat's retry loop. If a courier is offline at sending time, the message stays in the outbox. When any eligible courier comes online (e.g., a favorite peer appears nearby), the `courierBecameAvailable` method is invoked.

Inside `courierBecameAvailable`, the router:

1. Re-checks the pending messages in the outbox.
2. Builds a fresh list of eligible couriers.
3. Re‑deposits envelopes to those couriers, respecting the `maxCouriersPerMessage` limit (3) and the per‑envelope `copies` budget (8).

If a courier disconnects before delivering, the envelope remains in the outbox. No data is lost — the router simply waits for the next eligible courier to appear.

This "spray‑and‑wait" approach also throttles abuse: each envelope can be duplicated at most `CourierEnvelope.maxCopies` (8) times across all couriers, and any envelope older than `maxLifetimeSeconds` (24h) is silently dropped.

## Internet Fallback: When No Local Courier Is Enough

In sparse mesh topologies, physical couriers might never reach the recipient within the 24‑hour window. For that, BitChat includes **[`BridgeCourierService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BridgeCourierService.swift)**, which acts as an internet client.

This service:

- Takes a sealed `CourierEnvelope`.
- Publishes it as a **Nostr "drop"** — an anonymous encrypted event on the Nostr network.
- The recipient can later query Nostr for envelopes tagged with their rotating `recipientTag`.

This expands delivery beyond Bluetooth range, making courier mechanics work globally, while keeping all messages encrypted end‑to‑end.

## Key Files in the Source

| File | Role |
|------|------|
| [`CourierEnvelope.swift`](https://github.com/permissionlesstech/bitchat/blob/main/CourierEnvelope.swift) | Defines the TLV‑encoded envelope, recipient‑tag derivation, encoding/decoding, and copy‑budget logic. |
| [`MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouter.swift) | Orchestrates message routing, selects couriers, records deposits, and retries when couriers become available. |
| [`BridgeCourierService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BridgeCourierService.swift) | Provides the internet fallback – publishes sealed envelopes as Nostr drops. |
| [`MockTransport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MockTransport.swift) | Test harness that records `sentCourierMessages` to verify courier‑deposit behavior. |
| [`MessageRouterTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouterTests.swift) | Unit tests exercising courier selection, retry logic, and copy‑budget enforcement. |

## Summary

- **Couriers are regular peers** that transport encrypted `CourierEnvelope`s when the recipient is offline.
- The envelope is **TLV‑encoded** and has 24‑hour expiry, a rotating recipient tag, and a copy budget to thwart abuse.
- `MessageRouter` selects up to **3 couriers** and re‑deposits envelopes whenever a new eligible courier appears.
- The **`BridgeCourierService`** provides a Nostr‑based internet fallback, ensuring delivery even outside the mesh.
- The whole system is **privacy‑preserving** — couriers never see the sender, recipient, or content.

## Frequently Asked Questions

### What is the `Copies` limit in BitChat Couriers?

`Copies` is a budget that controls how many times a single `CourierEnvelope` may be distributed to different couriers. The default is `3` per message, but the envelope itself caps the absolute maximum at `8` (via `maxCopies`). This prevents message blasting across the network.

### How does the bPriority-encrypted recipient tag work?

The tag is a deterministic hash of the recipient's Noise static key and the current UTC day. It rotates daily, so the courier only sees a new opaque string each day. Only the intended recipient can match the tag to themselves, which hides the identity of the recipient from all couriers.

### Can a courier read the message?

No. The courier only stores and forwards opaque `ciphertext`. All encryption happens before creating the envelope, and the courier cannot decrypt the contents.

### What happens if the recipient comes back online but a courier is still offline?

If the recipient is already online, the sender can deliver directly, bypassing couriers. If the message is in the outbox for a courier, the sender will attempt a direct send first. If direct fails, the retry mechanism (`courierBecameAvailable`) continues to look for new couriers until the envelope expires (24h).