# How BitChat Manages Message Delivery and Read Receipts: A Deep Dive into the Swift Implementation

> Discover how BitChat implements message delivery and read receipts using its unique three-layer architecture. Explore the DeliveryStatus enum, ReadReceipt struct, and MessageRouter service for reliable communication.

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

---

**BitChat manages message delivery and read receipts through a three-layer architecture: the `DeliveryStatus` enum tracks message lifecycle states, the `ReadReceipt` struct encodes read acknowledgements in JSON or binary formats, and the `MessageRouter` service coordinates transport-agnostic routing with defensive reliability features.**

This article examines how the permissionlesstech/bitchat repository implements deterministic, end-to-end delivery tracking across heterogeneous transports including Nostr, Bluetooth Low Energy (BLE), and mesh networks. The design prioritizes defensive programming with validation, idempotency, and automatic cache pruning.

## Core Architecture: Three Components Working Together

### DeliveryStatus: The Message Lifecycle Enum

Every `BitchatMessage` carries a `deliveryStatus` property that drives the entire delivery pipeline. The enum is defined in [`localPackages/BitFoundation/Sources/BitFoundation/DeliveryStatus.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/DeliveryStatus.swift) and enumerates every phase from creation to final acknowledgement:

- `.notSentYet` — Initial state for standard messages
- `.sending` — Active transmission (used for private messages)
- `.sent` — Accepted by the local transport layer
- `.delivered(to: PeerID, at: Date)` — Confirmed arrival at recipient's inbox
- `.read(by: String, at: Date)` — Recipient viewed the message

This enum powers UI updates, triggers retransmission logic, and enables stale data pruning. The associated values capture precise metadata: which peer received the message and exactly when.

### ReadReceipt: Lightweight Acknowledgement Payload

The `ReadReceipt` struct in [`bitchat/Models/ReadReceipt.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Models/ReadReceipt.swift) represents the "I have read this message" signal. It contains:

- `originalMessageID`: UUID linking back to the source message
- `id`: Unique receipt identifier for deduplication
- `readerID`: The peer's `PeerID`
- `readerNickname`: Human-readable identifier
- `timestamp`: When the read occurred

The struct implements dual serialization strategies. **JSON encoding** (`encode()`/`decode()`) provides human-readable interoperability for Nostr relays and debugging. **Binary encoding** (`toBinaryData()`/`fromBinaryData()`) produces compact payloads optimized for bandwidth-constrained BLE and mesh transports.

### MessageRouter: Central Dispatch and State Coordination

[`bitchat/Services/MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MessageRouter.swift) serves as the transport-agnostic hub. It dispatches messages to appropriate backends, intercepts delivery acknowledgements, and routes incoming read receipts back to the UI layer.

The router maintains critical state including `sentReadReceipts: Set<String>` for idempotency—duplicate receipts with identical IDs are silently dropped.

## Delivery Flow: Step-by-Step

The complete message journey follows five discrete stages:

1. **Message creation** — `BitchatMessage` instantiates with `deliveryStatus = .notSentYet` (or `.sending` for private messages requiring immediate persistence).

2. **Transport routing** — `MessageRouter` selects the appropriate backend (Nostr relay, BLE peripheral, mesh node) and transmits the envelope. The transport emits a delivery acknowledgement when the message reaches a courier or peer inbox.

3. **Status advancement** — Upon ack, the router executes `message.deliveryStatus = .delivered(to: peerID, at: Date())`. This observable change propagates to the UI.

4. **Read receipt generation** — When the recipient views the message, their client constructs a `ReadReceipt` and returns it through the same router infrastructure.

5. **Receipt processing** — The router extracts `originalMessageID`, locates the matching `BitchatMessage`, and transitions its status to `.read(by: nickname, at: timestamp)`. The conversation UI reflects this immediately.

## Reliability Mechanisms

### Binary Encoding for Constrained Networks

The `ReadReceipt` binary format minimizes overhead for BLE and mesh scenarios where every byte counts. The `toBinaryData()` method serializes UUIDs as 16-byte raw values rather than 36-character strings, reducing payload size by approximately 55%.

### Defensive Parsing with Input Validation

The binary decoder in `ReadReceipt.fromBinaryData()` invokes `InputValidator.validateTimestamp()` and `validateNickname()` before constructing instances. Malformed or malicious payloads fail fast without corrupting conversation state.

### Idempotent Receipt Handling

Duplicate read receipts are suppressed via `sentReadReceipts: Set<String>`. The test suite in [`bitchatTests/Services/PrivateChatManagerTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/PrivateChatManagerTests.swift) verifies this behavior with assertions like `#expect(!manager.sentReadReceipts.contains("pm-unroutable"))`.

### Automatic Cache Pruning

The router periodically calls `pruneSentReadReceipts(keeping:)` to evict stale entries, preventing unbounded growth of the deduplication set. This matters for long-running chat sessions with thousands of messages.

## Practical Implementation: Creating and Sending Read Receipts

```swift
// 1. Create a read receipt when the user views a private message
let receipt = ReadReceipt(
    originalMessageID: message.id,
    readerID: transport.myPeerID,
    readerNickname: "Me"
)

// 2. Encode to binary for BLE transport (optional)
let binaryData = receipt.toBinaryData()

// 3. Send via the router — transport selection is automatic
await router.sendReadReceipt(receipt, to: peerID)

```

### Handling Incoming Receipts

```swift
func didReceiveReadReceipt(_ receipt: ReadReceipt) {
    // Look up the original message and update its status
    if let msg = conversation.message(withID: receipt.originalMessageID) {
        msg.deliveryStatus = .read(by: receipt.readerNickname, at: receipt.timestamp)
    }
}

```

## Key Source Files

| File | Purpose |
|------|---------|
| [`localPackages/BitFoundation/Sources/BitFoundation/DeliveryStatus.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/DeliveryStatus.swift) | Enum defining all delivery lifecycle states with associated metadata |
| [`localPackages/BitFoundation/Sources/BitFoundation/BitchatMessage.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/BitchatMessage.swift) | Core message model persisting `deliveryStatus` |
| [`bitchat/Models/ReadReceipt.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Models/ReadReceipt.swift) | Codable struct with JSON and binary serialization |
| [`bitchat/Services/MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MessageRouter.swift) | Central routing service coordinating transports and state updates |
| [`bitchatTests/Services/PrivateChatManagerTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/PrivateChatManagerTests.swift) | Unit tests validating delivery transitions and receipt propagation |
| [`bitchatTests/Mocks/MockTransport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Mocks/MockTransport.swift) | Test double verifying receipt-sending and pruning behavior |

## Summary

- **BitChat message delivery** relies on the `DeliveryStatus` enum to track precise lifecycle states from `.notSentYet` through `.read`.

- **Read receipts** use the `ReadReceipt` struct with dual serialization: JSON for Nostr compatibility, binary for BLE/mesh efficiency.

- **The `MessageRouter`** coordinates all transport backends, maintains idempotency via `Set<String>` deduplication, and automatically prunes stale cache entries.

- **Defensive design** includes input validation on binary parsing, observable state for reactive UI updates, and comprehensive test coverage in [`PrivateChatManagerTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PrivateChatManagerTests.swift).

## Frequently Asked Questions

### How does BitChat handle read receipts across different transport types?

The `MessageRouter` abstracts transport specifics. It serializes `ReadReceipt` to JSON for Nostr relays or binary for BLE/mesh, then dispatches through the appropriate backend. All transports converge on the same `didReceiveReadReceipt()` handler for uniform processing.

### What prevents duplicate read receipts from corrupting message state?

The router maintains `sentReadReceipts: Set<String>` containing unique receipt IDs. Incoming receipts are filtered against this set; duplicates are ignored. The cache is periodically pruned via `pruneSentReadReceipts(keeping:)` to control memory usage.

### Why does `ReadReceipt` support both JSON and binary encoding?

JSON provides human-readable debugging and compatibility with Nostr's text-based protocol. Binary encoding reduces payload size by approximately 55%—critical for bandwidth-constrained BLE and mesh networks where every byte affects battery life and latency.

### How does the UI know when to display "delivered" or "read" checkmarks?

`BitchatMessage.deliveryStatus` is an observable property. When the `MessageRouter` transitions it to `.delivered` or `.read`, the UI layer receives notification and renders the appropriate indicator. This reactive pattern decouples transport logic from presentation code.