How BitChat Routes Read Receipts and Delivery Acknowledgments: A Complete Technical Guide

BitChat routes read receipts and delivery acknowledgments through a multi-layered architecture where ReadReceipt structs are serialized to binary payloads, routed via MessageRouter to available transports (direct, mesh, or Nostr), and tracked in the view model using DeliveryStatus enums to prevent duplicate processing.

BitChat is an open-source peer-to-peer messaging framework that prioritizes reliable message delivery confirmation. Understanding how read receipts and delivery acknowledgments are routed through its Swift codebase reveals the architectural patterns that ensure end-to-end reliability across varying network topologies.

Read Receipt Data Model and Binary Serialization

The foundation of BitChat's acknowledgment system lives in bitchat/Models/ReadReceipt.swift. Here, the ReadReceipt struct captures five critical fields: the original message ID, a unique receipt ID, the reader’s PeerID, the reader’s nickname, and a timestamp.

This struct conforms to Codable for JSON transport but implements custom toBinaryData() and fromBinaryData(_:) methods for efficient network transmission. The binary format packs two 16-byte UUIDs (message ID and receipt ID), an 8-byte PeerID representation, a 64-bit timestamp, and the nickname string into a compact Data payload. The fromBinaryData method validates each field—checking UUID validity, peer ID integrity, timestamp ranges, and nickname constraints—before reconstructing the receipt.

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

// Encoding for network transmission
let binaryPayload = receipt.toBinaryData()

Transport Layer Routing and Abstraction

BitChat abstracts network complexity through the MessageRouter class. The method MessageRouter.sendReadReceipt(_:to:) resolves the appropriate reachable transport—whether direct peer-to-peer, mesh networking, or Nostr relay—and forwards the binary payload.

The transport implementations expose a unified sendReadReceipt(_:to:) interface. In test environments, bitchatTests/Mocks/MockTransport.swift demonstrates this contract by storing receipts in a sentReadReceipts array for verification, while production transports forward the binary data over active connections. This abstraction ensures that read receipts reach intended peers regardless of underlying connection type.

// Router selects the best available transport and sends
router.sendReadReceipt(receipt, to: peerID)

// Transport layer serializes and transmits
transport.sendReadReceipt(receipt, to: peerID)

Delivery Status Tracking with Enums

Delivery state management resides in localPackages/BitFoundation/Sources/BitFoundation/DeliveryStatus.swift. The DeliveryStatus enum defines the message lifecycle through cases including .sending, .sent, .delivered(to:at:), and .read(by:at:).

Each BitchatMessage maintains a deliveryStatus property that transitions through these states. When a transport confirms successful transmission, the status updates from .sending to .sent. Upon receiving a read receipt from the recipient, the system updates the corresponding message to .read, capturing the reader’s nickname and timestamp for display.

View-Model Receipt Management and Idempotency

The ChatViewModel class prevents duplicate processing through careful bookkeeping. It maintains two Set collections: sentReadReceipts (tracking IDs of receipts already transmitted) and sentDeliveryAcks (tracking message IDs with confirmed delivery).

Helper methods enforce idempotent operations:

  • markReadReceiptSent(_:) adds receipt IDs to the tracking set
  • pruneSentReadReceipts(keeping:) removes stale IDs to manage memory
  • didReceiveReadReceipt(_:) processes incoming receipts, updates the messageStore, and records the receipt ID to prevent reprocessing
func didReceiveReadReceipt(_ receipt: ReadReceipt) {
    // Update the matching message's status
    if var msg = messageStore[receipt.originalMessageID] {
        msg.deliveryStatus = .read(by: receipt.readerNickname, at: receipt.timestamp)
        messageStore[receipt.originalMessageID] = msg
    }
    
    // Prevent duplicate processing
    sentReadReceipts.insert(receipt.receiptID)
}

End-to-End Routing Flow

The complete routing of read receipts and delivery acknowledgments follows this deterministic sequence:

  1. Sender activation: After displaying a private message, the sender creates a ReadReceipt instance with the original message ID and local peer information.

  2. Router resolution: The view model invokes router.sendReadReceipt(receipt, to: peerID), which evaluates available network paths (direct, mesh, or relay) and selects the appropriate transport.

  3. Binary transmission: The chosen transport calls receipt.toBinaryData(), producing the compact binary payload containing UUIDs, peer ID, timestamp, and nickname.

  4. Network transmission: The payload travels across the selected protocol layer to the recipient.

  5. Receiver processing: The recipient deserializes the payload using ReadReceipt.fromBinaryData(data), updates the corresponding BitchatMessage.deliveryStatus to .read, and optionally transmits a delivery acknowledgment back to the sender.

  6. Idempotent tracking: Both ends maintain their respective tracking sets to ensure acknowledgments are processed exactly once, even in high-latency or reconnecting network conditions.

Summary

  • Binary efficiency: ReadReceipt uses custom toBinaryData() and fromBinaryData(_:) methods in ReadReceipt.swift to minimize network overhead.
  • Transport abstraction: MessageRouter.sendReadReceipt(_:to:) hides network complexity, routing receipts through direct, mesh, or Nostr transports as implemented in the test suite.
  • State management: The DeliveryStatus enum in BitFoundation provides type-safe tracking of message states from sending through read confirmation.
  • Duplicate prevention: ChatViewModel uses sentReadReceipts sets and helper methods like pruneSentReadReceipts(keeping:) to ensure idempotent receipt handling.
  • End-to-end reliability: The system guarantees delivery confirmation through structured serialization, abstracted routing, and persistent state tracking across peer connections.

Frequently Asked Questions

How does BitChat prevent duplicate read receipts from being processed?

BitChat prevents duplicate processing through the ChatViewModel class, which maintains a Set called sentReadReceipts containing unique receipt IDs. When didReceiveReadReceipt(_:) processes an incoming receipt, it inserts the receiptID into this set. Subsequent receipts with identical IDs are ignored, ensuring idempotent handling even if network conditions cause duplicate transmissions.

What transport protocols does BitChat use for routing read receipts?

According to the source code in bitchatTests/Services/MessageRouterTests.swift, the MessageRouter abstracts three transport types: direct peer-to-peer connections, mesh networking protocols, and Nostr relays. The router's sendReadReceipt(_:to:) method evaluates reachability and selects the appropriate transport, ensuring receipts reach recipients regardless of which underlying network stack is available.

How is delivery status represented in the BitChat data model?

Delivery status is represented by the DeliveryStatus enum defined in localPackages/BitFoundation/Sources/BitFoundation/DeliveryStatus.swift. This enum tracks message progression through states including .sending, .sent, .delivered(to:at:), and .read(by:at:). Each BitchatMessage maintains this status property, which updates as the transport layer confirms transmission and as read receipts arrive from recipients.

What validation does BitChat perform when decoding binary read receipts?

When decoding binary read receipts via ReadReceipt.fromBinaryData(_:) in bitchat/Models/ReadReceipt.swift, the system validates multiple fields: UUID format for both the original message ID and receipt ID, peer ID integrity (8-byte validation), timestamp range checks (64-bit validity), and nickname string constraints. This validation ensures that malformed or corrupted network payloads cannot instantiate invalid receipt objects.

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 →