# How Bitchat Handles Network Partitions and Disconnections: A Technical Deep Dive

> Discover how Bitchat masters network partitions and disconnections using three independent transport stacks. Learn about real-time monitoring, thread-safe caching, and automatic retries for robust communication.

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

---

**Bitchat detects and recovers from network partitions through three independent transport stacks—Nostr, BLE, and Geohash-based relays—each using Combine publishers for real-time state monitoring, thread-safe caching, and automatic retry scheduling with queued message persistence.**

The [bitchat](https://github.com/permissionlesstech/bitchat) decentralized messaging protocol must remain functional despite intermittent connectivity, Tor blocking, or Bluetooth mesh fragmentation. This article examines exactly how each transport layer in `permissionlesstech/bitchat` implements partition detection, state buffering, and seamless recovery.

## NostrTransport: Detecting Relay Partitioning

### How the DM Relay Connection Is Monitored

The **NostrTransport** class subscribes to `NostrRelayManager.shared.$isDMRelayConnected`, a `Combine` publisher that reports Tor-protected DM relay status. When this flips to `false`, the transport immediately knows the Internet path is severed.

From [[`bitchat/Services/NostrTransport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NostrTransport.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NostrTransport.swift), lines 62-66:

```swift
// The publisher that drives all Nostr connectivity state
relayConnectivityCancellable = NostrRelayManager.shared.$isDMRelayConnected
    .receive(on: queue)
    .sink { [weak self] connected in
        self?.relaysConnected = connected
        self?.reachablePeersDidChange()
    }

```

The `relaysConnected` flag is cached on a **concurrent dispatch queue**, allowing synchronous reads from the main actor while updates occur asynchronously.

### Message Queuing During Outages

When `relaysConnected` becomes `false`, `canDeliverPromptly` short-circuits and queues outbound messages. The same cancellable (lines 162-166) ensures the instant the relay reconnects, pending traffic resumes without user intervention.

Retry throttling is governed by `TransportConfig.uiGeoNotesConnectivityRetrySeconds`, defaulting to **3 seconds**.

## BLEService: Mesh Partition Detection

### Periodic Peer Reconciliation

Unlike Nostr's push-based model, **BLEService** actively polls peer health. Every ~30 seconds, `checkPeerConnectivity()` queries the underlying `BLEEngine` for `linkStates`—a dictionary mapping peer IDs to connection status.

From [[`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), lines 7073-7094:

```swift
func reconcileConnectivity(now: Date, linkStates: [PeerID: BLELinkState]) {
    registry.pruneStalePeers(asOf: now, linkStates: linkStates)
    let changes = registry.computeConnectivityChanges()
    
    // Emit events that trigger UI updates and message re-broadcast
    blePeerConnectivitySubject.send(changes)
    
    // Re-advertise presence when mesh health improves
    if changes.newReachablePeers.isEmpty == false {
        schedulePresenceBroadcast()
    }
}

```

Any peer missing from `linkStates` or marked `inactive` is treated as disconnected. The `BLEPeerRegistry` maintains the source of truth, and **stale peers are automatically re-advertised** when the mesh recovers, triggering message re-transmission.

## GeohashPresenceService: Geographic Relay Recovery

### Unified Connectivity Handling

**GeohashPresenceService** reuses the same `relayConnectivity` publisher as `NostrTransport`, calling `handleConnectivityChange()` on every state transition.

From [[`bitchat/Services/GeohashPresenceService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/GeohashPresenceService.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/GeohashPresenceService.swift), lines 184-208:

```swift
private func handleConnectivityChange(isConnected: Bool) {
    guard isConnected else {
        pendingGeohashUpdates.forEach { $0.markWaitingForNetwork() }
        return
    }
    
    // Connectivity restored—schedule batched retry
    scheduleConnectivityRetry(
        interval: TransportConfig.uiGeoNotesConnectivityRetrySeconds
    ) { [weak self] in
        self?.flushPendingUpdates()
    }
}

```

This ensures **geohash-based location broadcasts** survive temporary relay unavailability without duplicating effort or flooding the network.

## UI Feedback: ConnectivityStatusBanner

### Translating Low-Level State to User Action

The [[`ConnectivityStatusBanner.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ConnectivityStatusBanner.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Views/Components/ConnectivityStatusBanner.swift) component converts technical failures into actionable guidance:

```swift
enum ConnectivityIssue {
    case bluetoothPoweredOff
    case torBlocked
    case relayUnreachable
    
    static func resolve(
        bluetoothState: CBManagerState,
        torBlocked: Bool
    ) -> ConnectivityIssue? { ... }
}

```

The banner appears when **any transport** reports loss of connectivity and dismisses automatically when the respective flag recovers. This gives users immediate visibility while the protocol continues buffering outbound traffic transparently.

## Key Architectural Patterns

Bitchat's partition handling rests on four foundational patterns:

- **Publish-Subscribe State Propagation** — `Combine` publishers eliminate polling overhead and guarantee reactive, instant UI updates
- **Thread-Safe Caching** — Serial/concurrent queues protect `relaysConnected`, `reachablePeers`, and `BLEPeerRegistry` state without blocking the main actor
- **Graceful Degradation with Queuing** — Messages become `QueuedAck` objects in `NostrTransport` rather than failing; automatic retry resumes transmission post-recovery
- **Centralized Configuration** — `TransportConfig` (`uiGeoNotesConnectivityRetrySeconds`, `nostrReadAckInterval`) provides uniform retry behavior across all transports

## Practical Implementation Example

Observe and respond to connectivity changes in your own Bitchat integration:

```swift
import Combine
import Bitchat

class ResilientChatController: ObservableObject {
    @Published var issue: ConnectivityIssue?
    @Published var canSend = true
    
    private var cancellables = Set<AnyCancellable>()
    
    init(transport: NostrTransport) {
        // React to Nostr relay partitioning
        transport.$relaysConnected
            .map { $0 ? nil : .relayUnreachable }
            .assign(to: &$issue)
        
        // React to BLE mesh health
        BLEService.shared.peerConnectivityPublisher
            .map { changes in
                changes.disconnectedPeers.isEmpty ? nil : .localMeshDegraded
            }
            .assign(to: &$issue)
        
        // Drive send button state
        $issue
            .map { $0 == nil }
            .assign(to: &$canSend)
    }
}

```

## Summary

- **Three independent transports** (Nostr, BLE, Geohash) each monitor and recover from partitions autonomously
- **Combine publishers** provide real-time connectivity state without polling
- **Thread-safe caches** enable fast synchronous checks while maintaining background update safety
- **Automatic message queuing** prevents data loss during outages; retries resume seamlessly
- **Centralized `TransportConfig`** controls retry intervals uniformly
- **`ConnectivityStatusBanner`** surfaces actionable feedback without interrupting protocol operation

## Frequently Asked Questions

### How does Bitchat prevent message loss during a network partition?

Outbound messages are converted to `QueuedAck` objects and held in memory. When `relaysConnected` or mesh connectivity restores, the retry scheduler automatically re-attempts delivery. No user action is required.

### What triggers the connectivity status banner to appear?

The banner renders when `ConnectivityIssue.resolve()` detects `CBManagerState.poweredOff`, a Tor block flag, or relay unavailability from any active transport. It dismisses automatically once the underlying publisher reports restoration.

### Why does BLEService use polling instead of Combine publishers?

Bluetooth Low Energy link state changes are not uniformly exposable as publisher streams across iOS versions. The `checkPeerConnectivity()` interval (~30s) balances timely detection with battery impact.

### Can retry intervals be configured per-transport?

All transports read from `TransportConfig`. While there's a single source of truth, you can subclass or modify `uiGeoNotesConnectivityRetrySeconds` and `nostrReadAckInterval` to adjust behavior globally.