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

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) 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). 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/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:

// 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/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 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/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:

// 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:

// 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:

// 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 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →