# How BitChat Implements Read Receipts and Delivery Acknowledgments: A Complete Technical Breakdown

> Discover how BitChat implements read receipts and delivery acknowledgments. Explore the technical breakdown of data models, message routing, and state tracking for seamless communication.

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

---

**BitChat implements read receipts and delivery acknowledgments through a structured `ReadReceipt` data model with binary serialization, transport-layer routing via `MessageRouter`, and view-model state tracking using the `DeliveryStatus` enum.**

BitChat, a permissionless peer-to-peer messaging app, provides reliable message delivery confirmation through a carefully architected system of data models, transport abstractions, and state management. This article examines exactly how read receipts and delivery acknowledgments work in the [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat) codebase, from binary packet encoding to end-to-end flow completion.

## Read Receipt Data Model and Binary Encoding

The foundation of BitChat's read receipt system is the `ReadReceipt` struct defined in [`bitchat/Models/ReadReceipt.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Models/ReadReceipt.swift). This struct captures five critical fields:

- `originalMessageID`: UUID of the message being acknowledged
- `receiptID`: Unique identifier for this receipt itself
- `readerID`: 8-byte `PeerID` of the reader
- `readerNickname`: Display name for UI rendering
- `timestamp`: 64-bit Unix timestamp

The struct conforms to `Codable` for JSON persistence and JSON-based transport scenarios. More importantly, it provides custom binary encoding for low-level packet exchange through two key methods:

```swift
// Serialize to compact binary payload
let binaryData = receipt.toBinaryData()

// Deserialize from received payload
if let receipt = ReadReceipt.fromBinaryData(data) {
    // Process the receipt
}

```

The `toBinaryData()` method constructs a `Data` payload containing: two 16-byte UUIDs (original message ID and receipt ID), 8 bytes for the reader's `PeerID`, 8 bytes for the timestamp, and the UTF-8 encoded nickname string. The `fromBinaryData(_:)` validator enforces field integrity—checking UUID structure, peer ID validity, timestamp range bounds, and nickname presence—before constructing a valid `ReadReceipt` instance.

## Transport Layer and Message Routing

BitChat abstracts network transport through the `sendReadReceipt(_:to:)` protocol method. Actual implementations vary by network path:

| Transport Type | Implementation Location | Behavior |
|--------------|------------------------|----------|
| Mock (testing) | [`bitchatTests/Mocks/MockTransport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Mocks/MockTransport.swift) | Enqueues receipt in `sentReadReceipts` array for test verification |
| Direct peer-to-peer | Production transport layer | Forwards binary payload over direct connection |
| Mesh relay | Production transport layer | Routes through mesh network participants |
| Nostr relay | Production transport layer | Publishes to configured Nostr relays |

The `MessageRouter` class in [`MessageRouterTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouterTests.swift) (demonstrated via test-driven specifications) handles transport selection transparently. When `router.sendReadReceipt(_:to:)` is invoked, the router evaluates reachability and selects the appropriate transport—direct, mesh, or relay—guaranteeing receipt delivery regardless of underlying connection type.

```swift
// Router abstracts transport selection
router.sendReadReceipt(receipt, to: peerID)  // May use direct, mesh, or Nostr

```

## Delivery Status Tracking with DeliveryStatus Enum

Message delivery state is represented by the `DeliveryStatus` enum in [`localPackages/BitFoundation/Sources/BitFoundation/DeliveryStatus.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/DeliveryStatus.swift). This enum tracks the complete lifecycle:

- `.sending` — Message queued for transmission
- `.sent` — Handed to transport layer
- `.delivered(to:at:)` — Remote peer confirmed receipt
- `.read(by:at:)` — Remote peer sent read receipt back

Each `BitchatMessage` carries a `deliveryStatus` property that transitions through these states. Transport implementations update this status upon confirmation, enabling accurate UI indicators.

## View-Model State Management and Idempotency

The `ChatViewModel` maintains receipt handling state through two critical sets, as demonstrated in [`ChatViewModelDeliveryStatusTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatViewModelDeliveryStatusTests.swift):

- `sentReadReceipts`: `Set<UUID>` tracking receipts already transmitted (prevents duplicate sends)
- `sentDeliveryAcks`: `Set<UUID>` tracking messages with confirmed delivery

Three helper methods manage this state:

```swift
// Mark a receipt as sent to avoid retransmission
func markReadReceiptSent(_ receiptID: UUID)

// Clean up old receipt IDs to prevent unbounded memory growth
func pruneSentReadReceipts(keeping: Int)

// Process incoming receipt and update message status
func didReceiveReadReceipt(_ receipt: ReadReceipt)

```

The `didReceiveReadReceipt(_:)` implementation follows this pattern:

```swift
func didReceiveReadReceipt(_ receipt: ReadReceipt) {
    // Locate original message
    if var msg = messageStore[receipt.originalMessageID] {
        // Update to read status with reader metadata
        msg.deliveryStatus = .read(by: receipt.readerNickname, 
                                   at: receipt.timestamp)
        messageStore[receipt.originalMessageID] = msg
    }
    // Record processing to prevent duplicate handling
    sentReadReceipts.insert(receipt.receiptID)
}

```

## Complete End-to-End Flow

The full read receipt lifecycle spans five coordinated steps:

1. **Sender UI triggers read action** — When a user views a private message, the view model creates a `ReadReceipt` with `originalMessageID`, local `PeerID`, and nickname.

2. **Router selects transport** — `router.sendReadReceipt(receipt, to: peerID)` evaluates reachability and chooses the appropriate network path.

3. **Transport serializes and transmits** — The selected transport calls `receipt.toBinaryData()` and forwards the payload.

4. **Receiver deserializes payload** — `ReadReceipt.fromBinaryData(data)` validates and reconstructs the receipt.

5. **Receiver updates state and responds** — `chatViewModel.didReceiveReadReceipt(incoming)` marks the message as `.read` and may trigger a delivery acknowledgment back to the original sender.

This flow is exercised across the combined test suites in `ChatViewModelTests`, `MessageRouterTests`, and `MockTransport`, ensuring correctness across network condition variations.

## Summary

- **Read receipts** are modeled by `ReadReceipt` in [`bitchat/Models/ReadReceipt.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Models/ReadReceipt.swift) with dual JSON/binary serialization support
- **Binary encoding** optimizes network efficiency through compact `toBinaryData()`/`fromBinaryData(_:)` methods
- **Transport abstraction** via `MessageRouter` enables seamless operation across direct, mesh, and Nostr relay networks
- **Delivery status** lifecycle is tracked by `DeliveryStatus` enum with `.delivered` and `.read` confirmation states
- **Idempotent handling** prevents duplicate receipts through `sentReadReceipts` tracking and `pruneSentReadReceipts(keeping:)` cleanup

## Frequently Asked Questions

### How does BitChat prevent duplicate read receipts from being sent?

The `ChatViewModel` maintains a `sentReadReceipts` set containing UUIDs of all receipts already transmitted. Before sending, the view model checks against this set via `markReadReceiptSent(_:)`. The `pruneSentReadReceipts(keeping:)` method limits memory usage by retaining only recent IDs.

### What transport protocols does BitChat use for read receipt delivery?

According to the `MessageRouter` implementation, BitChat supports three transport paths: direct peer-to-peer connections, mesh network relays, and Nostr protocol relays. The router automatically selects the reachable transport for each recipient without caller intervention.

### How is the read receipt data structured at the binary level?

The `ReadReceipt.toBinaryData()` method produces a payload containing: 16 bytes for `originalMessageID` (UUID), 16 bytes for `receiptID` (UUID), 8 bytes for `readerID` (PeerID), 8 bytes for `timestamp` (UInt64), and variable-length UTF-8 bytes for `readerNickname`. This totals 48 bytes plus nickname length.

### Can delivery status be tracked for messages sent to offline peers?

Yes. The `DeliveryStatus` enum includes intermediate states `.sending` and `.sent` that apply before remote confirmation. When a peer comes online and receives the message, the status advances to `.delivered(to:at:)`. The sender's transport layer queues receipts and retry logic handles intermittent connectivity.