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 messageid: Unique receipt identifier for deduplicationreaderID: The peer'sPeerIDreaderNickname: Human-readable identifiertimestamp: 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:
-
Message creation —
BitchatMessageinstantiates withdeliveryStatus = .notSentYet(or.sendingfor private messages requiring immediate persistence). -
Transport routing —
MessageRouterselects 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. -
Status advancement — Upon ack, the router executes
message.deliveryStatus = .delivered(to: peerID, at: Date()). This observable change propagates to the UI. -
Read receipt generation — When the recipient views the message, their client constructs a
ReadReceiptand returns it through the same router infrastructure. -
Receipt processing — The router extracts
originalMessageID, locates the matchingBitchatMessage, 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
DeliveryStatusenum to track precise lifecycle states from.notSentYetthrough.read. -
Read receipts use the
ReadReceiptstruct with dual serialization: JSON for Nostr compatibility, binary for BLE/mesh efficiency. -
The
MessageRoutercoordinates all transport backends, maintains idempotency viaSet<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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →