Key Modules in BitChat's Application Layer: Runtime, Models, and Services

BitChat's application layer is organized around a composition root (AppRuntime) that injects feature-scoped view models—such as ConversationStore, ChatViewModel, and PublicChatModel—to separate domain logic from UI state and networking services.

BitChat, an open-source peer-to-peer messaging application maintained by permissionlesstech, implements a clean, layered architecture that separates runtime orchestration from domain models and view-specific state. Understanding the key modules in BitChat's application layer is essential for developers contributing to the SwiftUI codebase or integrating with its Nostr and mesh networking protocols. Each module is designed to eliminate the historic anti-pattern of a single global environment object, improving testability and maintainability.

Runtime Orchestration with AppRuntime

The AppRuntime class serves as the composition root for the entire application. Defined in bitchat/App/AppRuntime.swift, this ObservableObject owns the app-wide state and wires together all feature models, the ConversationStore, the ChatViewModel, and runtime services including Tor, notification handling, and shared-content import.

AppRuntime also publishes typed application events via AppEventStream, allowing decoupled components to react to lifecycle and networking changes. When the SwiftUI app launches, the runtime is injected into the environment and started automatically via ContentView.onAppear.

import Bitchat

// In the SwiftUI App entry point
@main
struct BitchatApp: App {
    @StateObject private var runtime = AppRuntime()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(runtime)                // inject the whole runtime
        }
    }
}

The AppRuntime composition root wires together all feature models and starts the networking stack when runtime.start() is called.

Domain State Management with ConversationStore

ConversationStore acts as the single source of truth for all conversation-level data. Located in bitchat/ViewModels/ConversationStore.swift, it centralizes public-channel timelines, private-inbox state, selected conversation tracking, unread counts, and message de-duplication.

All feature models read from and mutate this store via a well-defined intent API. The store exposes reactive publishers—such as publicMessages—that SwiftUI views subscribe to for real-time updates.

// In a view that shows the public chat timeline
struct PublicChatList: View {
    @EnvironmentObject var runtime: AppRuntime
    @State private var messages: [Message] = []

    var body: some View {
        List(messages) { msg in
            Text(msg.body)
        }
        .onReceive(runtime.conversations.publicMessages) { msgs in
            self.messages = msgs
        }
    }
}

ConversationStore publishes publicMessages (a Publisher<[Message]>) that UI components subscribe to.

UI Bridge and Feature-Specific Models

ChatViewModel as the Domain Coordinator

ChatViewModel functions as the primary bridge between domain logic and the UI. Implemented in bitchat/ViewModels/ChatViewModel.swift, it handles message sending, lifecycle events, and delegates heavy-lifting to coordinators like ChatPeerListCoordinator and ChatOutgoingCoordinator. It remains intentionally thin, with most state management deferred to the runtime-owned models.

Feature-Scoped View Models

BitChat employs feature-scoped models that are injected by AppRuntime, eliminating the monolithic global state pattern:

Sending a message through the public chat model demonstrates this delegation pattern:

// Inside a SwiftUI view that has `@EnvironmentObject var runtime: AppRuntime`
Button("Send") {
    let text = "Hello, mesh world!"
    // The public chat model forwards the intent to the ChatViewModel.
    runtime.publicChatModel.sendMessage(text)
}

PublicChatModel calls ChatViewModel.sendMessage(_:to:) with the canonical ConversationID for the public channel.

Nostr Protocol Integration

The Nostr layer provides decentralized identity and relay functionality through three key modules:

Accessing relay functionality follows this pattern:

// Example: fetch the latest Nostr events for the user's identity
func fetchLatestNostrEvents() async throws -> [NostrEvent] {
    let bridge = runtime.idBridge
    guard let identity = try bridge.getCurrentNostrIdentity() else { return [] }
    let relayMgr = runtime.chatViewModel.nostrRelayManager   // exposed via ChatViewModel
    return try await relayMgr.fetchEvents(for: identity.pubkey)
}

The NostrRelayManager encapsulates the connection and request logic; the app merely calls its high-level API.

Mesh Networking and Bluetooth Transport

For local mesh communication, BitChat integrates the Noise protocol and Bluetooth Low Energy (BLE) services:

These services operate below the application layer but are orchestrated by AppRuntime to provide seamless local connectivity alongside Nostr relay connectivity.

Application Chrome and Shared Content

Two specialized models handle UI-only state and system integration:

Updating the app chrome is straightforward:

runtime.appChromeModel.presentNicknameEditor = true

AppChromeModel holds UI-only flags such as presentNicknameEditor, showVerificationSheet, etc., keeping them out of the main ChatViewModel.

Summary

  • AppRuntime (AppRuntime.swift) serves as the composition root, wiring all services and view models together via dependency injection.
  • ConversationStore centralizes all conversation state, acting as the single source of truth for public channels and private inboxes.
  • Feature-scoped models (PublicChatModel, PrivateInboxModel, etc.) replace the monolithic global state pattern, improving testability.
  • NostrIdentityBridge and NostrRelayManager abstract the Nostr protocol, providing high-level APIs for decentralized identity and relay communication.
  • BLEService and NoiseProtocol handle encrypted local mesh networking, with hot-path components managed by the runtime.
  • AppChromeModel isolates UI-only presentation state from domain logic.

Frequently Asked Questions

What is the role of AppRuntime in BitChat's architecture?

AppRuntime acts as the composition root and sole ObservableObject injected into the SwiftUI environment. It instantiates and connects all feature models, stores, and networking services (Tor, BLE, Nostr), and publishes application-wide events via AppEventStream. This centralizes dependency management and ensures consistent lifecycle handling across the app.

How does ConversationStore prevent message duplication?

ConversationStore implements message de-duplication logic within its state management layer, acting as the single source of truth for all conversation data. By requiring all feature models to mutate conversation state through its well-defined intent API—rather than directly manipulating local arrays—it ensures that public channel timelines and private inboxes maintain consistent, duplicate-free message histories.

Why does BitChat use separate models instead of a single ChatViewModel?

The architecture has migrated away from a monolithic ChatViewModel (the historical global environment object) toward feature-scoped models such as PublicChatModel and PrivateConversationModel. This separation of concerns improves testability by allowing unit tests to inject mock stores, enhances performance by limiting view updates to relevant state changes, and increases maintainability by isolating domain logic from UI-specific state managed by AppChromeModel.

How does BitChat integrate with Nostr relays?

The application layer accesses Nostr functionality through the NostrIdentityBridge for key management and the NostrRelayManager for network operations. NostrRelayManager handles connection lifecycles, reconnection logic, and event routing, exposing high-level async methods like fetchEvents(for:) that the UI layer calls without managing raw JSON-RPC protocols directly.

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 →