# BitChat Client Architecture: A Deep Dive Into the SwiftUI-Based Messaging Stack

> Explore the BitChat client architecture, a SwiftUI messaging stack using ConversationStore and async events for BLE mesh, Tor, and Nostr networks. Learn how concerns are separated for a robust decentralized app.

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

---

**The BitChat client is a layered SwiftUI application that separates concerns into a composition root (`AppRuntime`), domain models backed by a centralized `ConversationStore`, and transport abstractions for BLE mesh, Tor, and Nostr networks, all communicating via typed async events.**

The BitChat client architecture in the [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat) repository implements a **clean architecture** pattern optimized for decentralized messaging. This Swift codebase organizes functionality into distinct layers: a central composition root that wires dependencies, feature-specific models that maintain UI state, and transport layers handling BLE mesh, Tor, and Nostr protocols. Understanding this architecture is essential for developers contributing to the permissionless messaging stack or building similar offline-first communication applications.

## Composition Root and Dependency Injection

The architecture centers on `AppRuntime`, a `@MainActor` `ObservableObject` defined in [`bitchat/App/AppRuntime.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/AppRuntime.swift). This class functions as the **composition root**, instantiating a single instance of every major component and storing them as `let` properties.

### The AppRuntime Class

`AppRuntime` owns three critical responsibilities:

- **Lifecycle Management**: The `start()` method kicks off networking services including `NetworkActivationService` and `GeohashPresenceService`
- **Dependency Wiring**: Constructs the `ConversationStore` (the single source of truth for message state) and injects it into feature models
- **Global Event Bus**: Maintains the `AppEventStream`, a typed async-await event bus that propagates app-wide events such as Nostr relay changes and Tor lifecycle notifications

According to the source comments in [`AppRuntime.swift`](https://github.com/permissionlesstech/bitchat/blob/main/AppRuntime.swift) (lines 17-20), this design provides "a single source of truth for conversation message state and selection."

### The AppEventStream

Rather than using delegate patterns throughout the stack, the architecture publishes typed `TransportEvent` objects through the `AppEventStream`. Feature models subscribe to these events using Combine or async-await, decoupling the UI from transport implementation details.

## Domain Layer and State Management

The domain layer consists of **focused feature models** that encapsulate state for specific UI areas. Each model reads from and writes to the central `ConversationStore`, eliminating duplicated state and synchronization bugs.

### ConversationStore as Single Source of Truth

Located in [`bitchat/App/ConversationStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/ConversationStore.swift), the `ConversationStore` maintains:

- All message timelines for public channels, private inboxes, and location-based channels
- Read state and unread counts
- The currently selected conversation identifier

All domain models receive a reference to this store during initialization, enabling authoritative reads and writes without state duplication.

### Feature-Specific Models

The codebase splits UI logic into focused model classes rather than using a monolithic ViewModel:

- **`PublicChatModel`** ([`bitchat/App/PublicChatModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/PublicChatModel.swift)): Reads the public channel timeline from `ConversationStore` and handles message sending
- **`PrivateInboxModel`** ([`bitchat/App/PrivateConversationModels.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/PrivateConversationModels.swift)): Exposes unread counts and inbox messages for private conversations
- **`PrivateConversationModel`** ([`bitchat/App/PrivateConversationModels.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/PrivateConversationModels.swift)): Manages individual DM threads and their Noise encryption sessions
- **`LocationChannelsModel`** ([`bitchat/App/LocationChannelsModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/LocationChannelsModel.swift)): Wraps `LocationChannelManager` to present geohash-based channels
- **`PeerListModel`** ([`bitchat/App/PeerListModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/PeerListModel.swift)): Provides peer presence data tied to location services
- **`AppChromeModel`** ([`bitchat/App/AppChromeModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/AppChromeModel.swift)): Stores UI chrome state including nickname editors, QR-scan UI, and privacy warnings

As noted in [`docs/ARCHITECTURE_V2.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/ARCHITECTURE_V2.md) (lines 21-24), this "new runtime/store layer gives us a safe place to move lifecycle, routing, and cross-cutting state before we split the UI into smaller feature models."

## Transport Layer Abstraction

The client abstracts three network backends through a common interface, each emitting typed events rather than using traditional delegate callbacks.

### BLE Mesh Implementation

The Bluetooth Low Energy stack centers on `BLEService`, implemented across `bitchat/Services/BLEService+LinkLayerCentralRole.swift` and related files. The architecture separates concerns into:

- **BLE Link Layer**: Handles CoreBluetooth delegate methods and serial mesh engine operations
- **Feature Modules**: Consume `TransportEvent` objects rather than accessing CoreBluetooth directly

The detailed design in [`docs/BLE-ARCHITECTURE-V3.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/BLE-ARCHITECTURE-V3.md) specifies that "transport code should emit typed events first, with delegate forwarding used only as a compatibility adapter during migration" (line 44).

### Tor and Nostr Integrations

- **`TorManager`**: Manages the Tor daemon lifecycle and provides `TorURLSession` for encrypted internet traffic
- **`NostrRelayManager`**: Handles decentralized relay connections for public chat functionality, defined alongside `NostrProtocol` implementations

Both services publish lifecycle events to the `AppEventStream`, allowing domain models to react to connectivity changes without direct dependencies on transport implementation.

### Typed Transport Events

All transports emit `TransportEvent` enums that traverse the architecture as follows:

1. Transport layer detects state change (e.g., BLE link up, Nostr relay connected)
2. Event posts to `AppEventStream`
3. `AppRuntime` observes via Combine and forwards to relevant domain models
4. Models update `@Published` properties, triggering SwiftUI view updates

## Security Layer (Noise Protocol)

Message confidentiality and authenticity across all transports utilize the **Noise Protocol** implementation located in `bitchat/Noise/`:

- **`NoiseSessionManager`** ([`bitchat/Noise/NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Noise/NoiseSessionManager.swift)): Handles session lifecycle and key negotiation
- **`SecureNoiseSession`**: Performs per-message encryption and decryption

These components are referenced by each transport implementation to ensure end-to-end encryption regardless of the underlying network (BLE, Tor, or Nostr).

## Concurrency Contracts and Thread Safety

The architecture enforces strict concurrency boundaries to prevent data races, documented in [`docs/BLE-ARCHITECTURE-V3.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/BLE-ARCHITECTURE-V3.md) (lines 55-58):

- **Engine-confined**: All mesh-engine state mutates on a single serial queue (`onEngine`)
- **BLE-queue-confined**: CoreBluetooth-specific state lives on its own dedicated queue
- **Lock-backed stores**: Reads crossing domain boundaries (e.g., peer registry) use locks preventing torn reads

Debug traps using `dispatchPrecondition` and unit tests in `BLEQueueContractTests` enforce these contracts during development.

## Practical Implementation Examples

Boot the client from your SwiftUI App struct:

```swift
import SwiftUI

@main
struct BitChatApp: App {
    @StateObject private var runtime = AppRuntime()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(runtime)
        }
        .onAppear {
            runtime.start()  // Kicks off networking and services
        }
    }
}

```

Send a public chat message from a view:

```swift
struct PublicChatView: View {
    @ObservedObject var publicChat: PublicChatModel
    
    var body: some View {
        // UI implementation
    }
    
    func sendMessage(_ text: String) {
        publicChat.sendMessage(text)  // Writes to ConversationStore → mesh/Nostr
    }
}

```

React to BLE link-layer events in a feature module:

```swift
import Combine

class PeerConnectionHandler {
    let runtime: AppRuntime
    var cancellables = Set<AnyCancellable>()
    
    init(runtime: AppRuntime) {
        self.runtime = runtime
        
        runtime.events
            .filter { $0 is AppEvent.linkUp }
            .sink { event in
                // Handle new peer connection
            }
            .store(in: &cancellables)
    }
}

```

Access the canonical store for testing or debugging:

```swift
let store = runtime.conversations
let selectedID = store.selectedConversationID  // Read-only view of selection state

```

## Summary

- **Composition Root**: `AppRuntime` ([`bitchat/App/AppRuntime.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/AppRuntime.swift)) initializes all dependencies and manages application lifecycle through the `start()` method
- **State Management**: `ConversationStore` ([`bitchat/App/ConversationStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/ConversationStore.swift)) serves as the single source of truth for all message timelines and selection state
- **Feature Models**: Specialized models like `PublicChatModel` and `PrivateInboxModel` encapsulate UI-specific logic without duplicating state
- **Transport Abstraction**: BLE mesh, Tor, and Nostr implementations emit typed `TransportEvent` objects through `AppEventStream` rather than using delegate callbacks
- **Security**: `NoiseSessionManager` provides encryption across all transport layers
- **Concurrency**: Strict contracts (Engine-confined, BLE-queue-confined, Lock-backed) enforce thread safety in the mesh networking layer

## Frequently Asked Questions

### What is the role of AppRuntime in BitChat?

`AppRuntime` serves as the composition root and global dependency container for the BitChat client. Annotated with `@MainActor` and conforming to `ObservableObject`, it creates single instances of all services, constructs the `ConversationStore`, and manages application lifecycle through its `start()` method. It also owns the `AppEventStream`, which decouples transport events from UI updates by acting as a typed async-await event bus.

### How does BitChat handle state management across multiple transports?

All transport implementations (BLE, Tor, Nostr) emit typed `TransportEvent` objects that propagate through the `AppEventStream`. Domain models subscribe to these events and update the centralized `ConversationStore`, which functions as the single source of truth. When users send messages, feature models write to `ConversationStore`, which routes encrypted payloads through the appropriate transport layer via `NoiseSessionManager`, ensuring consistent state regardless of which network layer delivers the message.

### What concurrency model does the BitChat BLE layer use?

The BLE architecture enforces three distinct concurrency contracts: Engine-confined state mutates on a single serial queue (`onEngine`), CoreBluetooth-specific state lives on a dedicated BLE queue, and cross-domain reads utilize lock-backed stores to prevent torn reads. These contracts are enforced through debug traps using `dispatchPrecondition` and verified by `BLEQueueContractTests`, ensuring thread safety in the mesh networking implementation documented in [`docs/BLE-ARCHITECTURE-V3.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/BLE-ARCHITECTURE-V3.md).

### How are transport events propagated to the UI layer?

Transport events flow through a unidirectional data stream: transport layers emit `TransportEvent` enums to the `AppEventStream`, which `AppRuntime` observes via Combine. The runtime forwards relevant events to feature models (like `PublicChatModel` or `PeerListModel`), which update their `@Published` properties. SwiftUI views observing these models via `@ObservedObject` or `@StateObject` automatically reflect state changes, maintaining a clear separation between transport implementation and UI presentation.