How BitChat's Courier System Handles Offline Messages: Store-and-Forward Architecture
BitChat uses a store-and-forward courier mechanism where messages are wrapped in encrypted, rotating-tagged envelopes and deposited on Nostr relays (kind‑1401) or carried by trusted physical couriers until the recipient reconnects.
BitChat, the open‑source peer‑to‑peer messaging protocol developed by permissionlesstech, solves the offline message delivery problem through a sophisticated courier architecture. When direct peer‑to‑peer delivery fails due to network unavailability, the system automatically transitions to store‑and‑forward mode using cryptographically sealed envelopes and daily‑rotating recipient tags to preserve privacy.
Courier Envelope Structure and Rotating Tags
The foundation of BitChat's offline messaging system lies in the CourierEnvelope structure defined in [localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift). Each envelope acts as an opaque TLV payload containing the encrypted message, an expiry timestamp, a copy budget, and a 16‑byte rotating recipient tag.
The recipient tag is derived from the peer's Noise static public key and the current UTC day using the CourierEnvelope.recipientTag(noiseStaticKey:epochDay:) method. Because the tag changes every 24 hours based on CourierEnvelope.epochDay(for:), observers cannot link drops addressed on different days to the same recipient without prior knowledge of that recipient's public key.
Bridge Courier Service: Internet‑Based Store‑and‑Forward
When the local MessageRouter cannot deliver a message directly, it invokes BridgeCourierService.shared.depositDrop(envelope:) to publish the envelope as a drop on Nostr relays as kind‑1401 events. The implementation in [bitchat/Services/Gateway/BridgeCourierService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/Gateway/BridgeCourierService.swift) handles this internet‑bridge pathway.
Each drop is signed with a fresh throw‑away key to prevent linking the sender to the envelope. Relays store these drops for up to 24 hours, as defined by CourierEnvelope.maxLifetimeSeconds. On the receiving side, the service watches the set of candidate tags—typically the tags for the current day and adjacent days—and opens matching drops via the openEnvelope closure to decrypt and process payloads.
Physical Courier Via Trusted Favorite Peers
Beyond internet relays, BitChat supports physical couriers—trusted favorite peers who remain online and carry envelopes for offline friends. When the sender's router cannot bridge a message, it advertises the envelope to its favorite couriers. The courier receives the envelope if the tag matches its watch list and physically transports it until the recipient comes online, at which point deliverToPeer forwards the packet to the local peer's transport layer. The courier never learns the envelope's contents; it matches only the opaque rotating tag.
End‑to‑End Delivery Flow
The complete offline message delivery workflow operates across three distinct phases:
- Sender creation. The
MessageRouterinstantiates aCourierEnvelopewith the recipient's tag, expiry, and encrypted ciphertext via theCourierEnvelope(recipientTag:expiry:ciphertext:copies:)initializer. - Bridge deposition. The sender calls
BridgeCourierService.shared.depositDrop(envelope:), which publishes a signed Nostr kind‑1401 event that remains on relays for up to 24 hours. - Receiver retrieval. The offline peer's
BridgeCourierServicesubscribes to its candidate tags; upon reconnection, it retrieves matching drops, invokesopenEnvelopeto decrypt the payload, and injects the message into the normal processing pipeline.
Creating and Publishing Offline Messages
The following Swift code demonstrates the sender's workflow for creating a courier envelope and depositing it via the bridge service:
import BitFoundation
import BitChat
// Create a courier envelope for an offline peer
let recipientStaticKey: Data = // peer's Noise static key
let epochDay = CourierEnvelope.epochDay(for: Date())
let tag = CourierEnvelope.recipientTag(
noiseStaticKey: recipientStaticKey,
epochDay: epochDay)
// Encrypt the message (Noise-X)
let ciphertext = // encrypted payload
let envelope = CourierEnvelope(
recipientTag: tag,
expiry: UInt64(Date().addingTimeInterval(24*60*60).timeIntervalSince1970 * 1000),
ciphertext: ciphertext,
copies: 2) // allow one re-spray by a courier
// Publish as a Nostr drop
BridgeCourierService.shared.depositDrop(envelope) { ok in
print("Drop published to relays:", ok)
}
Receiving and Forwarding Courier Envelopes
On the recipient side, the BridgeCourierService subscribes to candidate tags and processes incoming drops automatically:
// Receiver - open matching drops (usually called automatically)
BridgeCourierService.shared.openEnvelope = { env in
// Attempt decryption; return true if envelope handled
return try? MessageRouter.shared.processCourierEnvelope(env) != nil
}
For physical courier scenarios, the forwarding mechanism works as follows:
// Physical courier - forward a held envelope to a local peer
BridgeCourierService.shared.deliverToPeer = { env, peerID in
// Hand the envelope to the peer's transport layer
return PeerTransport.shared.send(env, to: peerID)
}
Summary
- Store‑and‑forward architecture: BitChat uses courier envelopes to deliver messages when peers are offline, supporting both internet bridges and physical trusted couriers.
- Rotating recipient tags: Derived daily from Noise static keys in
CourierEnvelope.swift, these 16‑byte tags enable routing while preventing linkability across days. - Nostr bridge integration: The
BridgeCourierServicepublishes encrypted drops as kind‑1401 events with throw‑away signatures, stored for 24 hours on relays. - Physical courier support: Favorite peers can carry envelopes for offline contacts without accessing message contents, using tag‑matching logic.
- Plausible deniability: The short, rotating tags ensure observers cannot identify recipients without knowing their public keys beforehand.
Frequently Asked Questions
How long does BitChat store offline messages on Nostr relays?
BitChat's courier system configures envelopes with a maximum lifetime of 24 hours using the maxLifetimeSeconds property. After this period, relays automatically expire kind‑1401 drops. Senders can specify shorter expiries when constructing CourierEnvelope instances, but the default bridge implementation enforces the 24‑hour upper bound to prevent indefinite storage bloat.
Can a physical courier read the contents of an offline message envelope?
No. Physical couriers—and bridge relays—handle only opaque CourierEnvelope payloads. The envelope contains the encrypted ciphertext produced via Noise‑X handshake sequences. Couriers match envelopes to recipients using only the 16‑byte rotating tag, which reveals nothing about the message content or the sender's identity without the recipient's private key.
What prevents observers from linking multiple drops to the same recipient?
BitChat implements daily rotating recipient tags calculated via CourierEnvelope.recipientTag(noiseStaticKey:epochDay:). Because the tag changes every UTC day based on the recipient's static public key, an observer monitoring Nostr relays cannot correlationally link drops sent on different days to the same peer. Additionally, senders sign each drop with fresh ephemeral keys, preventing sender‑linkability across multiple messages.
How does an offline recipient know which tags to watch when reconnecting?
The recipient's BridgeCourierService automatically generates candidate tags for the current UTC day and adjacent days using CourierEnvelope.candidateTags logic. When the peer reconnects, it subscribes to these specific tags on Nostr relays. This three‑day window accounts for clock skew and ensures messages sent just before the recipient went offline—or while they were disconnected—are still retrieved and processed.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →