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

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

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

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 Enum defining all delivery lifecycle states with associated metadata
localPackages/BitFoundation/Sources/BitFoundation/BitchatMessage.swift Core message model persisting deliveryStatus
bitchat/Models/ReadReceipt.swift Codable struct with JSON and binary serialization
bitchat/Services/MessageRouter.swift Central routing service coordinating transports and state updates
bitchatTests/Services/PrivateChatManagerTests.swift Unit tests validating delivery transitions and receipt propagation
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.

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.

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 →