# How File Transfer Works Over BLE Mesh in BitChat: A Complete Technical Guide

> Discover how BitChat enables file transfer over BLE mesh. Learn about its layered architecture, payload fragmentation, reassembly, and security handlers in this technical guide.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-05

---

**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](https://github.com/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.

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

```swift
// 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](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/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/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](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/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

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFileTransferHandler.swift) 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/BLEIncomingFileStore.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEIncomingFileStore.swift) implements crash-safe persistence through a **two-phase commit** mechanism:

1. Write file data to a temporary path with a deterministic marker
2. Record completion status in UserDefaults
3. 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:

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

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

| File | Purpose | GitHub Link |
|------|---------|-------------|
| [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) | Core BLE orchestration: link management, fragmentation, queue coordination | [[`/bitchat/Services/BLE/BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main//bitchat/Services/BLE/BLEService.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift) |
| [`BLEFileTransferHandler.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFileTransferHandler.swift) | Inbound packet processing: policy application, validation, storage orchestration, UI delivery | [[`/bitchat/Services/BLE/BLEFileTransferHandler.swift`](https://github.com/permissionlesstech/bitchat/blob/main//bitchat/Services/BLE/BLEFileTransferHandler.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFileTransferHandler.swift) |
| [`BLEFileTransferPolicy.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFileTransferPolicy.swift) | Routing decisions: self-echo detection, delivery plan generation, broadcast filtering | [[`/bitchat/Services/BLE/BLEFileTransferPolicy.swift`](https://github.com/permissionlesstech/bitchat/blob/main//bitchat/Services/BLE/BLEFileTransferPolicy.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFileTransferPolicy.swift) |
| [`BLEIncomingFileStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEIncomingFileStore.swift) | Persistent storage: quota enforcement, panic recovery, file lifecycle management | [[`/bitchat/Services/BLE/BLEIncomingFileStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main//bitchat/Services/BLE/BLEIncomingFileStore.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEIncomingFileStore.swift) |
| [`BLEFileTransferPolicyTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFileTransferPolicyTests.swift) | Unit tests verifying self-echo suppression and routing correctness | [[`/bitchatTests/Services/BLEFileTransferPolicyTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main//bitchatTests/Services/BLEFileTransferPolicyTests.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/BLEFileTransferPolicyTests.swift) |
| [`BLE-ARCHITECTURE-V3.md`](https://github.com/permissionlesstech/bitchat/blob/main/BLE-ARCHITECTURE-V3.md) | Design documentation: concurrency contracts, san-IO architecture, security considerations | [[`/docs/BLE-ARCHITECTURE-V3.md`](https://github.com/permissionlesstech/bitchat/blob/main//docs/BLE-ARCHITECTURE-V3.md)](https://github.com/permissionlesstech/bitchat/blob/main/docs/BLE-ARCHITECTURE-V3.md) |

---

## 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**: `bleQueue` handles only link-layer I/O; `messageQueue` handles only engine state; stores use private serial queues with lock-backed access
- **Deterministic cleanup**: `BLEIncomingFileStore` registers all in-flight files with panic markers, enabling complete state reconstruction after crashes

These constraints are documented in [BLE-ARCHITECTURE-V3.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/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.

```swift
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 `BLEOutboundFragmentTransferScheduler` handling outbound chunking and `BLEFragmentAssemblyBuffer` managing inbound reassembly
- **Policy enforcement** in `BLEFileTransferPolicy` prevents routing loops through self-echo detection and determines delivery routing for private versus public media
- **Crash-safe storage** via `BLEIncomingFileStore` implements 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.