# How BitChat Tracks Read Receipts and Delivery Status in Its Decentralized Network

> Discover how BitChat tracks read receipts and delivery status in its decentralized network. Learn about peer-to-peer state machines, signed receipts, and zero central servers for reliable messaging.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: how-to-guide
- Published: 2026-08-09

---

**BitChat tracks read receipts and delivery status through a peer-to-peer state machine combining `DeliveryStatus` enums, cryptographically-signed `ReadReceipt` objects, and coordinator classes that synchronize acknowledgments across mesh and courier transports without centralized servers.**

In the `permissionlesstech/bitchat` repository, the messaging protocol achieves end-to-end reliability without central servers by implementing a sophisticated tracking system for read receipts and delivery status. This decentralized approach ensures that senders know when messages reach recipients and when they are actually opened, all while maintaining the privacy guarantees of peer-to-peer encryption.

## Core Architecture for Message Tracking

BitChat implements read receipts and delivery status tracking through three tightly integrated components that operate entirely within the peer-to-peer network.

### The DeliveryStatus State Machine

The **`DeliveryStatus`** enum defined in [[`localPackages/BitFoundation/Sources/BitFoundation/DeliveryStatus.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/DeliveryStatus.swift)](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/DeliveryStatus.swift) acts as a state machine that records the complete lifecycle of every message. Each `BitchatMessage` initializes with a `deliveryStatus` of `.notSentYet` and transitions through distinct states:

- `.sending` – Actively being encrypted and transmitted
- `.sent` – Handed to the transport layer (mesh, courier, or Nostr)
- `.carried` – Specifically for courier hand-off scenarios
- `.delivered` – Reached the recipient's device
- `.read` – Opened and viewed by the recipient
- `.failed` or `.partiallyDelivered` – Error states for network failures

Each state transition stores the peer that caused the change and a precise timestamp, creating an immutable audit trail stored locally in the `ConversationStore`.

### The ReadReceipt Cryptographic Object

When a recipient opens a message, the system generates a **`ReadReceipt`** struct as defined in [[`bitchat/Models/ReadReceipt.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Models/ReadReceipt.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Models/ReadReceipt.swift). This lightweight, cryptographically-typed object contains:

- The original message ID being acknowledged
- A unique receipt ID for deduplication
- The reader's `PeerID` and nickname
- The read timestamp

The receipt supports dual serialization: `encode()` produces JSON for debugging, while `toBinaryData()` creates a compact binary blob optimized for bandwidth-constrained mesh networks. All receipts travel back to the sender through the same encrypted Noise `XX` session used for the original message.

## How the Coordinator Stack Manages Receipt Flow

The coordinator pattern bridges the UI layer, transport layer, and persistent store to handle the complex logic of generating, sending, and processing receipts in a decentralized environment.

### Outbound Receipt Generation

The [[`PrivateChatManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PrivateChatManager.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/PrivateChatManager.swift) orchestrates the decision logic for when to generate read receipts. When a user opens an unread message, the manager creates a `ReadReceipt` instance and passes it to the active transport:

```swift
// Create a read receipt when a message is opened
let receipt = ReadReceipt(
    originalMessageID: message.id,
    readerID: myPeerID,
    readerNickname: "Alice"
)

// Send the receipt over the active transport (e.g. mesh)
await transport.sendReadReceipt(receipt, to: message.senderPeerID)

```

Before transmission, the system checks the `sentReadReceipts` set (a `Set<String>` persisted via `UserDefaults`) to prevent duplicate acknowledgments across app restarts.

### Inbound Receipt Processing

The [[`ChatDeliveryCoordinator.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatDeliveryCoordinator.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatDeliveryCoordinator.swift) serves as the central hub for processing inbound acknowledgments. When a receipt arrives via `didReceiveReadReceipt(_:)`, the coordinator executes `updateAcknowledgedMessageDeliveryStatus(_:status:from:)`:

1. Verifies the receipt type is `.delivered` or `.read`
2. Calls `markMessageDelivered(_:from:)` to update the specific message
3. Invokes `context.setDeliveryStatus(status, forMessageID:…, inDirectPeerAliases:)` to atomically update the `ConversationStore`
4. Triggers `notifyUIChanged()` to refresh SwiftUI views
5. Releases media retry state via `confirmPrivateMediaDelivery`

The [`ConversationStore`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ConversationStore/ConversationStore.swift) enforces a "no-downgrade" rule, ensuring that once a message reaches `.read` status, it cannot revert to `.delivered`.

### Persistence and Deduplication

The [[`ChatViewModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatViewModel.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatViewModel.swift) manages the `sentReadReceipts` cache to avoid network spam. On initialization, it calls `ChatViewModelBootstrapper.loadPersistedReadReceipts(userDefaults:)` to restore the set of already-sent receipt IDs:

```swift
// Inspect the persisted set of sent receipts (used to dedupe)
let persisted = ChatViewModelBootstrapper.loadPersistedReadReceipts(
    userDefaults: UserDefaults.standard
)
print("Already‑sent receipts: \(persisted)")

```

To prevent unbounded growth, `ChatDeliveryCoordinator.cleanupOldReadReceipts()` periodically invokes `pruneSentReadReceipts(keeping:)` to remove receipt IDs whose underlying messages have been deleted from the local store.

## Code Implementation Examples

For testing or debugging delivery flows, developers can manually force status transitions:

```swift
// Manually force a status transition (useful for testing)
chatDeliveryCoordinator.updateMessageDeliveryStatus(
    "msg‑12345",
    status: .delivered(to: "Bob", at: Date())
)

```

Querying the current state of any message follows this pattern:

```swift
// Query the current delivery state of a message
if let status = chatDeliveryCoordinator.deliveryStatus(for: message.id) {
    print("Current status: \(status.displayText)")
}

```

## Summary

- **State Machine Tracking**: The `DeliveryStatus` enum in [`DeliveryStatus.swift`](https://github.com/permissionlesstech/bitchat/blob/main/DeliveryStatus.swift) provides immutable state transitions from `.notSentYet` through `.read` with peer and timestamp attribution.
- **Cryptographic Receipts**: `ReadReceipt` structs contain message IDs, unique receipt IDs, and reader credentials, serialized as either JSON or compact binary for transport efficiency.
- **Decentralized Coordination**: The coordinator stack (`ChatDeliveryCoordinator`, `ChatViewModel`, `PrivateChatManager`) handles generation, transmission, and processing without central servers.
- **Deduplication Strategy**: A `Set<String>` named `sentReadReceipts` persisted in `UserDefaults` prevents duplicate acknowledgments across app sessions.
- **Automatic Pruning**: `cleanupOldReadReceipts()` removes stale receipt IDs to maintain storage efficiency as conversations evolve.

## Frequently Asked Questions

### How does BitChat prevent duplicate read receipts?

BitChat prevents duplicate read receipts by maintaining a `sentReadReceipts` set in `UserDefaults`, which the `ChatViewModel` checks before transmitting any acknowledgment. This persistence ensures that even if the app restarts, the system will not resend receipts for messages already acknowledged, and the `cleanupOldReadReceipts()` method periodically prunes entries for deleted messages to prevent unbounded storage growth.

### What transport protocols carry read receipts in BitChat?

Read receipts travel over the same encrypted channels as regular messages—specifically the Noise `XX` session tunnels—utilizing the mesh, courier, or Nostr transport layers depending on network availability. The receipts are serialized using `toBinaryData()` for bandwidth efficiency when traversing peer-to-peer mesh networks, or standard JSON encoding when debugging.

### How is delivery status stored locally?

Delivery status is stored atomically in the `ConversationStore`, indexed by message ID and peer aliases, with the store enforcing a strict "no-downgrade" policy where statuses can only progress toward `.read`. The `ChatDeliveryCoordinator` calls `setDeliveryStatus(status:forMessageID:inDirectPeerAliases:)` to update these records, which then propagate to the SwiftUI layer via `notifyUIChanged()`.

### Can read receipts be forged in BitChat's decentralized network?

Read receipts are cryptographically tied to the reader's `PeerID` and signed within the Noise `XX` encrypted session, making forgery computationally infeasible without compromising the recipient's private keys. Additionally, the `DeliveryStatus` state machine in the sender's `ConversationStore` validates that receipt transitions come from authenticated peers within the active encrypted tunnel.