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:
PublicChatModel(bitchat/ViewModels/PublicChatModel.swift): Manages the public chat view, providing paginated message lists and reacting to broadcast events.PrivateInboxModel(bitchat/ViewModels/PrivateInboxModel.swift): Represents the private-inbox state, including DM lists and unread counts, reading directly fromConversationStore.PrivateConversationModel(bitchat/ViewModels/PrivateConversationModel.swift): Handles selected direct-message conversations, including peer identity resolution, encryption state, and favorite toggling.ConversationUIModel(bitchat/ViewModels/ConversationUIModel.swift): Centralizes composer state, autocomplete, message formatting, and row-level actions, keeping the UI decoupled fromChatViewModellogic.LocationChannelsModel(bitchat/ViewModels/LocationChannelsModel.swift): Adapts the location-presence subsystem, exposing geohash-based channels and mesh-only presence data.PeerListModel(bitchat/ViewModels/PeerListModel.swift): Supplies the peer-list UI with live counts and routing information, consumingLocationChannelsModelrather than accessing the location manager directly.
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:
NostrIdentityBridge(bitchat/Nostr/NostrIdentityBridge.swift): Bridges the app's internal identity representation with Nostr protocol specifics (npub/nostr keys), used byAppRuntimeand various Nostr services.NostrRelayManager(bitchat/Nostr/NostrRelayManager.swift): Manages relay connections, handling reconnection logic, proof-of-work (PoW), and inbound/outbound event routing.NostrProtocol(bitchat/Nostr/NostrProtocol.swift): Implements the low-level Nostr protocol, including JSON-RPC subscription handling and event publishing.
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:
NoiseProtocol(bitchat/Noise/NoiseProtocol.swift): Provides encrypted mesh networking capabilities.BLEService(bitchat/Services/BLEService.swift): Owns the hot-path components includingBLEOutboundWriteBuffer,BLEIngressLinkRegistry, andBLEFanoutSelector, exposing them to the application through the runtime.
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:
AppChromeModel(bitchat/ViewModels/AppChromeModel.swift): Manages presentation state for nickname editing, fingerprint routing, privacy UI, and screenshot handling. Keeping these flags in a dedicated model prevents bloat inChatViewModel.SharedContentImportModel(bitchat/ViewModels/SharedContentImportModel.swift): Handles importing text and media from the Share Extension, persisting data viaSharedContentStore.BoardAlertsModel(bitchat/ViewModels/BoardAlertsModel.swift): Generates system-line alerts for mesh-only and geohash-specific messages usingBoardStore.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →