# How BitChat Handles Network Connectivity Issues: A Deep Dive into Offline-First Design

> Discover how BitChat overcomes network connectivity issues with its offline first design. Learn about its unique approach to Bluetooth-mesh messaging and internet-dependent operations.

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

---

**BitChat handles network connectivity issues by isolating offline Bluetooth-mesh messaging from online Nostr/Tor transport, using a centralized NetworkActivationService that gates internet-dependent operations based on real-time reachability monitoring and user preferences.**

This architecture ensures that mesh communication remains functional during internet outages while preserving battery life and enabling automatic recovery when connectivity returns. Let's examine how the permissionlesstech/bitchat repository implements this dual-transport approach.

## The Core Architecture: Separation of Concerns

BitChat's connectivity handling rests on a fundamental principle: **mesh messaging and internet messaging are independent layers**. The Bluetooth-mesh layer operates continuously, while Tor bootstrapping and Nostr relay connections are strictly controlled.

Two Swift services orchestrate this behavior:

- **NetworkReachabilityMonitor** — provides debounced, coarse-grained reachability signals
- **NetworkActivationService** — decides when internet-dependent work may proceed

This design prevents wasted battery cycles on mesh-only devices and eliminates connection thrashing during network flapping.

## NetworkReachabilityMonitor: Debounced OS Signals

In [[`bitchat/Services/NetworkReachabilityMonitor.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NetworkReachabilityMonitor.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NetworkReachabilityMonitor.swift), the monitor wraps Apple's `NWPathMonitor` with **debouncing logic** to filter transient network changes.

The `isReachable` property reports `false` only when `NWPath.Status.unsatisfied` indicates no usable network interface. Rapid connectivity fluctuations are suppressed, ensuring that:

- Tor bootstrap sequences aren't interrupted by momentary drops
- Relay connections don't disconnect/reconnect wastefully
- Battery consumption remains predictable

The monitor publishes changes via Combine, making it trivial to observe from any SwiftUI view model or service.

## NetworkActivationService: The Central Gate

[[`bitchat/Services/NetworkActivationService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NetworkActivationService.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NetworkActivationService.swift) contains the core decision logic. Its `effectiveAllowed` property combines **three independent conditions**:

1. **Base policy requirements** — user has location permission, at least one mutual favorite exists, or the user is inside a location channel
2. **Network reachability** — the debounced `isReachable` flag from `NetworkReachabilityMonitor`
3. **User Tor preference** — the persisted boolean `userTorEnabled`

When `activationAllowed` transitions to `true`, the service triggers:

```swift
relayController.connect()
torController.startIfNeeded()  // if userTorEnabled == true

```

When it drops to `false`, it calls:

```swift
relayController.disconnect()
torController.shutdownCompletely()

```

This gate prevents Tor initialization attempts on airplanes, in tunnels, or when the user explicitly disabled it.

## Controller Conformance: Tor and Relay Integration

The service doesn't own transport implementations directly. Instead, it depends on two protocols:

| Protocol | Default Implementation | Responsibility |
|----------|------------------------|--------------|
| `NetworkActivationTorControlling` | `TorManager` ([[`localPackages/Arti/Sources/TorManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/Arti/Sources/TorManager.swift)](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/Arti/Sources/TorManager.swift)) | Tor bootstrap, proxy configuration, circuit management |
| `NetworkActivationRelayControlling` | `NostrRelayManager` ([[`localPackages/BitFoundation/Sources/BitFoundation/NostrRelayManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/NostrRelayManager.swift)](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/NostrRelayManager.swift)) | WebSocket connections to Nostr relays |

This protocol-based design allows testing with mock controllers and supports future transport additions without modifying `NetworkActivationService`.

## UI Integration: Exposing State to Views

The `ChatViewModel` bridges service state to the SwiftUI layer. In [[`bitchat/ViewModels/ChatViewModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatViewModel.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatViewModel.swift), the view model subscribes to published properties:

```swift
class ChatViewModel: ObservableObject {
    @Published private(set) var networkActivationAllowed = false
    private var cancellable: AnyCancellable?

    init() {
        cancellable = NetworkActivationService.shared.$activationAllowed
            .receive(on: DispatchQueue.main)
            .assign(to: \.networkActivationAllowed, on: self)
    }
}

```

The `networkActivationAllowed` property drives:

- **Offline indicators** — shown when the mesh operates alone
- **Connecting states** — displayed during Tor bootstrap
- **Network glyphs** — appended to bridged messages indicating relay routing

For Tor-specific feedback, [`ChatViewModel+Tor.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/Extensions/ChatViewModel+Tor.swift) provides descriptive messages for bootstrap failures, including network-level blocks.

## Automatic Recovery and Message Queuing

When connectivity returns, the recovery flow executes automatically:

1. `NetworkReachabilityMonitor` detects `NWPath.Status.satisfied` and publishes `isReachable = true`
2. `NetworkActivationService.reevaluate()` recalculates `effectiveAllowed`
3. If conditions permit, relays reconnect and Tor restarts (per user preference)
4. Queued messages awaiting Nostr delivery transmit through the restored path

Messages sent via `ChatCoordinator.shared.sendMessage(_:)` automatically route through Bluetooth when the internet gate is closed, ensuring **zero-downtime messaging** for mesh-connected peers.

## Complete Working Example

Here's how to integrate connectivity handling in a custom view model:

```swift
import Combine

class CustomChatViewModel: ObservableObject {
    @Published private(set) var networkActivationAllowed = false
    @Published private(set) var torEnabled = false
    
    private var cancellables = Set<AnyCancellable>()
    private let activationService = NetworkActivationService.shared
    
    init() {
        // Observe network gate
        activationService.$activationAllowed
            .receive(on: DispatchQueue.main)
            .assign(to: &$networkActivationAllowed)
        
        // Observe Tor preference
        activationService.$userTorEnabled
            .receive(on: DispatchQueue.main)
            .assign(to: &$torEnabled)
    }
    
    func sendMessage(_ text: String) async {
        // Coordinator handles transport selection automatically
        await ChatCoordinator.shared.sendMessage(text)
    }
    
    func toggleTorPreference(_ enabled: Bool) {
        activationService.setUserTorEnabled(enabled)
    }
    
    var connectionStatus: String {
        if networkActivationAllowed {
            return torEnabled ? "Connected via Tor" : "Connected (relays only)"
        }
        return "Offline (mesh only)"
    }
}

```

## Key Implementation Files

| File | Lines of Interest | Purpose |
|------|-------------------|---------|
| [`NetworkActivationService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NetworkActivationService.swift) | `effectiveAllowed` computed property, `reevaluate()` | Central gating logic |
| [`NetworkReachabilityMonitor.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NetworkReachabilityMonitor.swift) | `NWPathMonitor` integration, debounce | Reachability signal |
| [`ChatViewModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatViewModel.swift) | Combine subscriptions | UI state bridge |
| [`TorManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/TorManager.swift) | `startIfNeeded()`, `shutdownCompletely()` | Tor lifecycle |
| [`NostrRelayManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrRelayManager.swift) | `connect()`, `disconnect()` | Relay WebSocket management |

## Summary

- **BitChat separates mesh and internet transports completely** — Bluetooth messaging never depends on connectivity state
- **NetworkReachabilityMonitor debounces OS signals** to prevent thrashing during network flapping
- **NetworkActivationService combines policy, reachability, and preference** into a single gate that controls Tor and relay operations
- **Protocol-based controller design** enables testing and future transport extensions
- **Automatic recovery** reconnects relays and restarts Tor when conditions permit, with queued message delivery

## Frequently Asked Questions

### How does BitChat behave when the device has no internet connection at all?

The mesh layer continues operating normally. `NetworkActivationService` detects `isReachable == false`, keeps the gate closed, and suppresses all Tor and relay activity. Messages route exclusively through Bluetooth to nearby peers. No error states surface to users — the experience degrades gracefully to offline-first functionality.

### Can users force Tor usage even on poor networks?

Users set `userTorEnabled` via [`setUserTorEnabled(_:)`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NetworkActivationService.swift), but this is only one input to the gate. The base policy and reachability requirements must also satisfy. This prevents Tor bootstrap timeouts and battery drain on genuinely offline devices.

### What happens to messages queued during connectivity loss?

Messages submitted to `ChatCoordinator` persist in a local queue. When `activationAllowed` becomes true, the service triggers reconnection, and queued messages automatically transmit via the appropriate transport. Bridged messages (mesh → internet) carry network glyphs indicating their routing path.

### How is battery consumption optimized during network instability?

The **debounced reachability monitor** ignores sub-second network flaps, preventing rapid Tor stop/start cycles. Additionally, `shutdownCompletely()` terminates Tor processes entirely when gated off, rather than maintaining idle circuits. This design prioritizes **offline battery endurance** over instant reconnection latency.