How the Bitchat Client-Side Architecture Is Structured: A Layer-by-Layer Guide
Bitchat's client is a Swift iOS/macOS application organized into six distinct layers—UI, ViewModels, Core Services, Cryptography, Sync, and Data Models—enabling secure, decentralized messaging over multiple transport protocols.
This article examines the client-side architecture of bitchat, an open-source peer-to-peer chat client. Understanding how the bitchat client-side is structured reveals how the app achieves clean separation between presentation logic, business operations, and cryptographic operations while supporting simultaneous BLE mesh, Nostr, and TCP/UDP transports.
Architectural Overview
The bitchat client-side architecture follows MVVM (Model-View-ViewModel) with heavy reliance on dependency injection and protocol-oriented design. Every major component is replaceable, making the codebase testable and adaptable to new transport mechanisms.
The six primary layers are:
- Views — SwiftUI components that render conversation lists, message threads, and composer interfaces
- ViewModels —
ObservableObjectclasses that bridge user actions to service layer operations - Core Services — High-level business logic for networking, encryption, routing, and presence
- Cryptography — Noise protocol implementation for end-to-end encryption
- Sync & Gossip — Peer-to-peer state replication and message deduplication
- Data Models & Identity — Plain Swift structs defining domain entities and cryptographic identity management
App Launch and Dependency Injection
The entry point BitchatApp.swift establishes the entire dependency graph. Unlike many iOS apps that rely on singletons, bitchat constructs a fresh AppRuntime on launch that instantiates all services and injects them into view-models.
According to the source in BitchatApp.swift, the bootstrap sequence:
- Creates
Transport,NoiseEncryptionService,GossipSyncManager, andGeohashPresenceService - Passes these to
ChatViewModelBootstrapper.build() - Assigns the resulting
ChatViewModelto the root view's environment
// Simplified representation of the bootstrap flow
let runtime = AppRuntime()
let services = runtime.instantiateServices()
let viewModel = ChatViewModelBootstrapper.build(with: services)
rootView.environmentObject(viewModel)
This injection pattern keeps view-models free of hardcoded dependencies, allowing identical view code to run against mocks in previews and tests.
ViewModels: State Management and Coordination
The bitchat client-side structure places significant logic in view-models. ChatViewModel.swift serves as the central observable state holder, but the architecture delegates specialized concerns to coordinator objects.
ChatViewModelBootstrapper.swift wires approximately seven coordinators:
| Coordinator | Responsibility |
|---|---|
ChatPublicConversationCoordinator |
Public channel message flows |
ChatPrivateConversationCoordinator |
Direct message handling |
ChatMediaTransferCoordinator |
Image/audio upload and download |
ChatComposerCoordinator |
Input validation and draft management |
GeoPresenceTracker |
Location-based peer discovery |
BluetoothAlertCoordinator |
BLE permission and state handling |
Each coordinator conforms to a protocol, enabling test doubles and alternative implementations.
Example: Sending a Message Through the ViewModel
struct MessageComposer: View {
@ObservedObject var chatVM: ChatViewModel
@State private var text = ""
var body: some View {
HStack {
TextField("Type a message…", text: $text)
Button("Send") {
// Triggers: validation → MessageRouter → encryption → transport
chatVM.sendMessage(text: text, media: nil)
text = ""
}
}
}
}
The sendMessage method in ChatViewModel constructs a BitchatMessage struct, then delegates routing to MessageRouter.swift.
Core Services: Routing and Transport Abstraction
The service layer in bitchat's client-side architecture abstracts all networking complexity. MessageRouter.swift implements the central routing decision engine.
When sendMessage reaches the router, it evaluates:
- Destination type — public group, private conversation, or broadcast
- Available transports — BLE mesh proximity, Nostr relay connectivity, or direct TCP/UDP
- Encryption requirements — Noise session existence with recipient
// Conceptual router logic based on MessageRouter.swift implementation
func route(message: BitchatMessage, to destination: Destination) {
if canUseBLEMesh(destination) {
sendViaBLE(message)
} else if nostrRelayAvailable {
sendViaNostr(message)
} else {
sendViaTransport(message) // TCP/UDP fallback
}
}
Transport.swift provides the underlying abstraction. Concrete implementations handle platform-specific networking while presenting a uniform send(data:to:) interface.
Cryptography Layer: Noise Protocol Implementation
Bitchat's client-side security depends on Noise/SecureNoiseSession.swift, implementing the Noise Protocol Framework for zero-round-trip encryption.
NoiseEncryptionService.swift manages the session lifecycle:
- Creates
SecureNoiseSessioninstances on first contact with a peer - Rotates keys according to Noise patterns
- Handles pre-key bundling from
Identity/SecureIdentityStateManager.swift
Each session maintains cryptographic state independent of the transport carrying its packets. This separation means encrypted messages survive transport switching—a message may encrypt over Noise, fragment for BLE, fail, retry over Nostr, and decrypt identically at the recipient.
Sync and Gossip: Decentralized State Replication
The bitchat client-side structure includes sophisticated peer-to-peer synchronization via Sync/GossipSyncManager.swift and Sync/RequestSyncManager.swift.
These components implement epidemic broadcast for consistency without central servers:
GossipSyncManagerperiodically exchangesRequestSyncPacketstructures with discovered peersRequestSyncManagerhandles incoming sync requests and constructs delta responsesSync/SyncResponseRateLimiter.swiftprevents sync flooding
The sync layer operates below message routing but above raw transport, ensuring conversation history converges even when participants intermittently connect through different network paths.
Identity and Persistence
Cryptographic identity lives in Identity/SecureIdentityStateManager.swift, which:
- Generates X25519 key pairs on first launch
- Batches pre-keys for offline peer initialization
- Persists secrets via
KeychainManager.swift
The keychain integration uses iOS SecItem APIs with kSecAttrAccessibleAfterFirstUnlock for background message decryption. Group membership and message content use plain Swift structs (Models/BitchatPeer.swift, Models/BitchatMessage+Media.swift) serialized to the app's private storage.
Key Source Files
| File Path | Architectural Role |
|---|---|
BitchatApp.swift |
Application entry and dependency graph construction |
ViewModels/ChatViewModel.swift |
Central observable state holder |
ViewModels/ChatViewModelBootstrapper.swift |
Factory for view-models and coordinators |
ViewModels/GeohashSubscriptionManager.swift |
Location-based presence observation |
Services/Transport.swift |
Multi-protocol networking abstraction |
Services/MessageRouter.swift |
Message routing logic across transports |
Services/NoiseEncryptionService.swift |
Noise session management |
Services/GeohashPresenceService.swift |
Geographic peer discovery |
Noise/SecureNoiseSession.swift |
Concrete Noise protocol implementation |
Sync/GossipSyncManager.swift |
Peer-to-peer gossip synchronization |
Identity/SecureIdentityStateManager.swift |
Cryptographic identity lifecycle |
Models/BitchatMessage+Media.swift |
Message data model with media support |
Utils/MessageDeduplicator.swift |
Duplicate message suppression |
Summary
- Bitchat's client-side architecture separates UI, state, services, cryptography, sync, and persistence into distinct layers
- MVVM with coordinators keeps view code simple while isolating complex business logic
- Dependency injection throughout enables testing and transport flexibility
- Noise protocol implementation in
SecureNoiseSession.swiftprovides modern encryption - Gossip-based sync allows decentralized operation without central servers
- Multi-transport support (BLE, Nostr, TCP/UDP) is transparent to UI code through
Transport.swiftabstraction
Frequently Asked Questions
What architecture pattern does bitchat use?
Bitchat uses MVVM with coordinator objects. SwiftUI views observe ObservableObject view-models, which delegate to specialized coordinators for concerns like public conversations, private messages, media transfer, and Bluetooth state. This pattern keeps views thin and testable.
How does bitchat handle multiple transport protocols?
The MessageRouter.swift service evaluates destination type, network availability, and encryption state to select among BLE mesh, Nostr relays, or direct TCP/UDP. The Transport.swift abstraction hides protocol specifics from higher layers.
Where is the encryption implemented in bitchat?
End-to-end encryption lives in Noise/SecureNoiseSession.swift, implementing the Noise Protocol Framework. NoiseEncryptionService.swift manages session creation and key rotation, while Identity/SecureIdentityStateManager.swift handles long-term key storage in the iOS keychain.
How does bitchat synchronize messages without a server?
Sync/GossipSyncManager.swift implements epidemic broadcast: peers exchange RequestSyncPacket structures to discover missing messages. The protocol converges on consistent history through pairwise synchronization, with rate limiting in Sync/SyncResponseRateLimiter.swift to control bandwidth.
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 →