How File Transfer Works Over BLE Mesh in BitChat: A Complete Technical Guide
BitChat transports files across its Bluetooth Low Energy mesh using a layered architecture that fragments large payloads, reassembles them on the receiving device, and enforces security policies through dedicated handlers for validation, storage, and UI delivery.
The BitChat messaging protocol implements san-IO design principles to move binary media—images, voice notes, documents—across a decentralized BLE mesh without centralized servers. This guide examines the complete file transfer pipeline, from outbound fragmentation through inbound validation and crash-safe storage, using source-level references from the permissionlesstech/bitchat repository.
File Transfer Architecture Overview
BitChat's BLE mesh separates concerns into three distinct domains: the link-layer (raw BLE operations), the engine queue (protocol state and packet processing), and lock-backed stores (persistent storage). This separation eliminates deadlocks and ensures deterministic single-writer semantics for all protocol state.
The file transfer pipeline comprises ten distinct stages, each implemented by dedicated Swift components:
| Stage | Component | Responsibility |
|---|---|---|
| 1. Packet creation | BitchatPacket + FilePayload |
Structure binary data with metadata |
| 2. Fragmentation | BLEOutboundFragmentTransferScheduler |
Chunk data to fit BLE MTU limits |
| 3. Link-layer delivery | BLELinkStateStore |
Transmit raw bytes over BLE connections |
| 4. Reassembly | BLEFragmentAssemblyBuffer |
Reconstruct packets from fragments |
| 5. Policy enforcement | BLEFileTransferHandler + BLEFileTransferPolicy |
Apply routing and echo-suppression rules |
| 6. Validation | BLEIncomingFileValidator |
Check size, MIME type, and format |
| 7. Storage | BLEIncomingFileStore |
Persist files with quota enforcement |
| 8. Receipt handling | BLEPrivateMediaReceiptStore |
Acknowledge private media delivery |
| 9. UI delivery | deliverMessage closure |
Present media to users |
| 10. Cleanup | BLEIncomingFileStore |
Remove temporary files and update state |
All engine operations execute on the serial messageQueue, while BLE callbacks funnel raw bytes through ingestDecodedPacket to maintain thread safety.
Stage-by-Stage File Transfer Flow
Outbound: Creating and Sending Files
File transfer begins when the application constructs a BitchatPacket containing a FilePayload. The payload includes the raw bytes, MIME type, stable message identifier, and timestamp. For private media, the packet is encrypted via the active Noise session; public broadcasts use signed payloads.
import Foundation
// Build a file payload (e.g., a captured photo)
let imageData = try Data(contentsOf: capturedImageURL)
let payload = FilePayload(
data: imageData,
mime: "image/jpeg",
messageID: UUID().uuidString,
timestamp: Date().timeIntervalSince1970 * 1000
)
// Construct the full packet with sender metadata
let packet = BitchatPacket(
payload: payload,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
senderPeerID: localPeerID,
// Additional fields for routing and authentication
)
The packet enters the fragmentation system through [BLEService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift)'s outbound pipeline. The BLEOutboundFragmentTransferScheduler divides packets into fragments of 512 bytes or less to accommodate BLE MTU constraints:
// BLEService.swift -- outbound transmission entry point
func send(packet: BitchatPacket) {
// Packet is queued for fragmentation and transmission
outboundFragmentTransfers.schedule(packet, on: bleQueue)
}
Each fragment receives a sequence number and total packet identifier, enabling lossless reassembly. The scheduler writes fragments to available BLE links (central or peripheral roles) without blocking the calling thread.
Inbound: Reassembly and Policy Processing
On the receiving device, BLEService.swift maintains a BLEFragmentAssemblyBuffer that accumulates fragments until the complete packet is reconstructed. Once reassembly finishes, the packet transfers to the messageQueue for protocol processing.
[BLEFileTransferHandler.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFileTransferHandler.swift) receives reassembled packets and executes two critical policy checks defined in BLEFileTransferPolicy.swift:
- Self-echo detection: Drops packets whose sender matches the local peer, preventing routing loops
- Delivery plan determination: Routes private media to the intended recipient, broadcasts public messages, or ignores filtered content
// BLEFileTransferHandler.swift -- core inbound processing
func handle(_ packet: BitchatPacket, from senderPeerID: PeerID) -> Bool {
// Stage 5: Policy enforcement
let policy = BLEFileTransferPolicy.deliveryPlan(
packet: packet,
localPeerID: localPeerID
)
guard policy != .ignore else { return false }
guard !BLEFileTransferPolicy.isSelfEcho(packet, localPeerID: localPeerID) else {
return false // Suppress looped packets
}
// Stage 6: Payload validation
try BLEIncomingFileValidator.validate(packet.payload)
// Stage 7: Storage with quota check
try environment.enforceStorageQuota(reservingBytes: payload.data.count)
// ... continuation to storage and delivery
}
Validation, Storage, and Recovery
The BLEIncomingFileValidator enforces size limits, MIME type allowlisting, and format integrity before any bytes touch persistent storage. This protects against malformed payloads and resource exhaustion attacks.
[BLEIncomingFileStore.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEIncomingFileStore.swift) implements crash-safe persistence through a two-phase commit mechanism:
- Write file data to a temporary path with a deterministic marker
- Record completion status in UserDefaults
- Atomically move to final location after verification
The store maintains a default 100 MiB quota and supports panic recovery through PanicRecoveryError handling, ensuring incomplete transfers never leave orphaned media:
// BLEIncomingFileStore.swift -- quota-aware storage
func saveIncomingFile(
_ data: Data,
preferredName: String?,
subdirectory: String,
fallbackExtension: String,
defaultPrefix: String
) throws -> URL {
try enforceQuota(reserving: data.count)
let marker = createPanicRecoveryMarker()
defer { clearMarker(marker) }
let targetURL = buildStorageURL(
prefix: defaultPrefix,
subdirectory: subdirectory,
preferredName: preferredName,
fallbackExtension: fallbackExtension
)
try data.write(to: targetURL, options: .atomic)
commitMarker(marker, for: targetURL)
return targetURL
}
Private Media Receipts and Acknowledgment
For encrypted private media, the handler creates a receipt through BLEPrivateMediaReceiptStore. The [BLEFileTransferHandler.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFileTransferHandler.swift) records receipt state via commitPrivateMediaFile, deferring sender acknowledgment until successful UI delivery:
// BLEFileTransferHandler.swift -- private media flow
if policy == .privateMedia {
let receipt = BLEPrivateMediaReceipt(
messageID: packet.messageID,
senderPeerID: senderPeerID,
fileURL: storedURL,
timestamp: packet.timestamp
)
receiptStore.commit(receipt)
// Deliver to UI; acknowledgment sent upon confirmation
deliverMessage(.privateMedia(receipt)) { success in
if success { self.acknowledgeDelivery(receipt) }
}
}
Key Implementation Files
Concurrency and Safety Guarantees
BitChat's file transfer implementation adheres to strict architectural constraints that prevent the "ABBA" deadlock patterns found in earlier BLE mesh implementations:
- Single-writer discipline: All protocol state mutation occurs on
engineQueue; BLE callbacks never touch protocol state directly - Queue isolation:
bleQueuehandles only link-layer I/O;messageQueuehandles only engine state; stores use private serial queues with lock-backed access - Deterministic cleanup:
BLEIncomingFileStoreregisters all in-flight files with panic markers, enabling complete state reconstruction after crashes
These constraints are documented in BLE-ARCHITECTURE-V3.md and enforced through the Swift actor model and dispatch queue hierarchies.
Complete File Transfer Example
The following code demonstrates end-to-end file transfer: outbound creation, fragmentation, and simulated inbound processing with storage verification.
import XCTest
// MARK: - Outbound: Send a voice note
func testVoiceNoteTransfer() throws {
let audioData = try Data(contentsOf: voiceNoteURL)
let payload = FilePayload(
data: audioData,
mime: "audio/opus",
messageID: "voice-\(UUID().uuidString)",
duration: 15.3
)
let packet = BitchatPacket(
payload: payload,
timestamp: currentTimestamp(),
senderPeerID: sender.id,
recipientPeerID: recipient.id // nil for broadcasts
)
// Transmit via BLE mesh
bleService.send(packet: packet)
}
// MARK: - Inbound: Simulate received packet processing
func testInboundProcessing() throws {
let simulatedPacket = BitchatPacket(
payload: FilePayload(
data: testImageData,
mime: "image/webp",
messageID: "test-msg-001"
),
timestamp: currentTimestamp(),
senderPeerID: remotePeerID
)
// Handler executes full pipeline: policy → validation → storage
let handled = fileTransferHandler.handle(
simulatedPacket,
from: remotePeerID
)
XCTAssertTrue(handled)
// Verify file persisted with correct metadata
let storedFiles = incomingFileStore.listStoredFiles()
XCTAssertEqual(storedFiles.count, 1)
XCTAssertEqual(storedFiles.first?.mimeType, "image/webp")
}
Summary
- BitChat file transfer over BLE mesh relies on packet fragmentation to accommodate 512-byte MTU limits, with
BLEOutboundFragmentTransferSchedulerhandling outbound chunking andBLEFragmentAssemblyBuffermanaging inbound reassembly - Policy enforcement in
BLEFileTransferPolicyprevents routing loops through self-echo detection and determines delivery routing for private versus public media - Crash-safe storage via
BLEIncomingFileStoreimplements two-phase commits with panic recovery markers, quota enforcement, and automatic cleanup - Receipt acknowledgment for private media defers sender notification until UI delivery confirmation, ensuring reliable end-to-end transfer semantics
- Architectural boundaries between link-layer, engine queue, and persistent stores eliminate deadlocks and maintain deterministic single-writer semantics
Frequently Asked Questions
What is the maximum file size BitChat can transfer over BLE?
BitChat does not enforce a fixed maximum file size at the protocol level. The practical limit depends on storage quota configuration (default 100 MiB per device) and mesh path reliability. Large files experience higher failure rates due to BLE link instability. The BLEIncomingFileStore quota check throws before accepting transfers that would exceed available space, and applications can configure custom quotas through the enforceStorageQuota injection point.
How does BitChat prevent duplicate file deliveries?
Duplicate suppression operates at two levels. Packet-level deduplication uses the stable messageID field in BitchatPacket to reject already-processed packets. Self-echo detection in BLEFileTransferPolicy.isSelfEcho drops packets that originated from the local peer and looped through the mesh. The receipt store additionally tracks acknowledgment state for private media, preventing re-delivery of successfully received files.
What happens when a file transfer is interrupted mid-transfer?
Interrupted transfers leave incomplete fragments in the BLEFragmentAssemblyBuffer, which expires entries based on age thresholds. For partially stored files, BLEIncomingFileStore panic recovery detects orphaned markers on next launch and purges incomplete data. The sender receives no acknowledgment, triggering application-level retry logic. No partial file data reaches the UI layer.
Can BitChat transfer files to multiple recipients simultaneously?
Yes, through mesh flooding for public broadcasts or multi-unicast for private media. The BLEFileTransferPolicy.deliveryPlan returns .broadcast for public packets, causing the handler to forward to all connected peers. For private media addressed to multiple recipients, the application constructs individual encrypted packets with distinct recipientPeerID values; the mesh forwards each through optimal paths without revealing the recipient set to intermediate nodes.
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 →