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

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), 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) 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:

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

When it drops to false, it calls:

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)) 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)) 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), the view model subscribes to published properties:

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 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:

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 effectiveAllowed computed property, reevaluate() Central gating logic
NetworkReachabilityMonitor.swift NWPathMonitor integration, debounce Reachability signal
ChatViewModel.swift Combine subscriptions UI state bridge
TorManager.swift startIfNeeded(), shutdownCompletely() Tor lifecycle
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(_:), 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →