# BitChat ConversationStore and AppRuntime Architecture: A Deep Dive into Message State Management

> Explore BitChat's ConversationStore and AppRuntime architecture. Learn how O(1) incremental updates and ObservableObject conversations streamline message state management for efficient chat applications.

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

---

**BitChat centralizes all message state in a single `ConversationStore` singleton owned by the `@MainActor` `AppRuntime`, providing O(1) incremental updates via a thin intent API while publishing changes through `ObservableObject` conversations.**

The permissionlesstech/bitchat repository implements a centralized message state architecture that eliminates legacy synchronization complexity. Understanding how the **ConversationStore** and **AppRuntime** components interact is essential for developers building on or contributing to this peer-to-peer messaging platform.

## ConversationStore: The Single Source of Truth

The **ConversationStore** holds every message, unread count, and delivery status for every conversation type, including public, mesh, geohash-based, and direct chats. It acts as the sole writer to message state, ensuring consistent reads and incremental updates without full-dictionary rebuilds.

### Singleton Design and Main Actor Concurrency

The store is an **@MainActor** **singleton** instantiated by `AppRuntime` during application launch (see [`bitchat/App/AppRuntime.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/AppRuntime.swift)). This design guarantees that all mutations occur on the main thread, eliminating race conditions in the UI layer. Because the store lives on the main actor, a call to any mutation returns only after the new state is observable, ensuring consistent reads.

### The Conversation Model and ObservableObjects

Each conversation is modeled by a reference-type **ObservableObject** called **Conversation**. A `Conversation` owns an incremental **message-ID index** and maintains a capped message array constrained by **TransportConfig** caps to prevent unbounded memory growth. These objects publish their own `messages` and unread state via `@Published` properties, allowing UI components to subscribe to granular updates rather than observing the entire `messagesByConversation` dictionary.

### Intent API for Single-Writer Semantics

The store exposes a **thin intent API** that enforces **single-writer** semantics:

- `append(_:to:)` – Adds a new message to a specific conversation
- `upsertByID(_:in:)` – Updates or inserts a message by ID
- `setDeliveryStatus(_:for:in:)` – Updates delivery tracking
- `markRead(_:)` and `markUnread(_:)` – Manages read receipts
- `migrateConversation(from:to:)` – Handles conversation transitions
- `clear(_:)` – Removes conversation data

All internal collections are marked `private(set)`, ensuring external code can read but never mutate directly. This guarantees that every state change flows through the intent surface, making side effects predictable and debuggable.

### Publishing Strategy for UI and Background Consumers

The store employs a dual-publishing strategy to serve both UI and non-UI consumers. Each **Conversation** publishes changes via its own `@Published` properties, while the store exposes a global `changes: PassthroughSubject<ConversationChange, Never>` stream. Background services such as delivery tracking and background sync subscribe to this subject to receive fine-grained events like "message appended in conversation X" or "status changed," without triggering unnecessary UI refreshes.

## AppRuntime: The Lifecycle Orchestrator

The **AppRuntime** is the top-level component that owns the **ConversationStore** and lives on the main actor. It serves as the entry point for the app’s lifecycle, bootstrapping the UI, initializing services (transport, verification, peer management), and wiring the store to the rest of the system.

### Store Ownership and Initialization

On launch, `AppRuntime` creates the **ConversationStore** instance. Coordinators and view-models (such as `ChatViewModel` and `PrivateInboxModel`) obtain a reference to this store and either issue intents for writes or subscribe to its `Conversation` objects for reads. This ownership model eliminates the need for dependency injection containers or global variable abuse, as the runtime provides the authoritative store reference.

### Decoupling from Legacy Synchronization

Because the **ConversationStore** is the sole writer, **AppRuntime** no longer requires the bridge-based synchronization loops that previously performed full-dictionary replacements. The architecture has removed legacy stores including `PublicTimelineStore`, `PrivateChatManager`, and `LegacyConversationStore`, with feature models now deriving their view data directly from the unified store. This consolidation reduces state duplication and eliminates an entire class of synchronization bugs.

## High-Level Message Flow

The interaction between components follows a strict unidirectional flow:

1. **Incoming message** triggers `ChatViewModel` (or an appropriate coordinator) to call `store.append(message, to: conversationID)`.
2. **ConversationStore** inserts the message incrementally, updates the per-conversation ID index, and publishes the change through both the specific `Conversation` object and the global `changes` stream.
3. **UI components** observing that `Conversation` receive the update instantly, while background services listening to `store.changes` react for delivery tracking or notification dispatch.

## Implementation Examples

Appending a message to a direct conversation:

```swift
let store = ConversationStore.shared   // owned by AppRuntime
let newMessage = ChatMessage(...)
store.append(newMessage, to: .directPeer(peerID))

```

Observing a specific conversation in a SwiftUI view:

```swift
@ObservedObject var convo: Conversation   // injected from the store
Text(convo.messages.last?.text ?? "")

```

Marking a message as read:

```swift
store.markRead(messageID)

```

## Key Implementation Files

| Component | File | Description |
|-----------|------|-------------|
| **Conversation Store** | [`bitchat/App/ConversationStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/ConversationStore.swift) | Core implementation with intent API and singleton |
| **Conversation Model** | [`bitchat/App/Conversation.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/Conversation.swift) | ObservableObject representing individual chats |
| **Store Tests** | [`bitchatTests/ConversationStoreTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/ConversationStoreTests.swift) | Verification of intent API and state invariants |
| **Design Document** | [`docs/CONVERSATION-STORE-DESIGN.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/CONVERSATION-STORE-DESIGN.md) | Architectural rationale and decisions |
| **Runtime Entry** | [`bitchat/App/AppRuntime.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/AppRuntime.swift) | Main actor orchestrator that creates the store |
| **Architecture Overview** | [`docs/ARCHITECTURE_V2.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/ARCHITECTURE_V2.md) | High-level system architecture documentation |

## Summary

- **ConversationStore** is an `@MainActor` singleton that acts as the single source of truth for all message state, owned by the **AppRuntime**.
- The store uses **single-writer semantics** via a thin intent API (`append`, `upsertByID`, `setDeliveryStatus`, etc.) to guarantee consistent, incremental updates.
- Each **Conversation** is an **ObservableObject** with `@Published` properties, enabling O(1) per-conversation updates without full-dictionary rebuilds.
- **AppRuntime** initializes the store and eliminates legacy synchronization by removing `PublicTimelineStore`, `PrivateChatManager`, and other deprecated components.
- The architecture supports both UI observation (via `Conversation` objects) and background processing (via `PassthroughSubject<ConversationChange, Never>`).

## Frequently Asked Questions

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

**AppRuntime** is the top-level `@MainActor` component that initializes and owns the **ConversationStore**. It bootstraps the application lifecycle, initializes transport and verification services, and provides the central store reference to coordinators and view-models, eliminating the need for complex dependency injection or global state managers.

### How does ConversationStore handle concurrent updates?

The store uses Swift's **@MainActor** annotation to ensure all mutations occur on the main thread. Because it operates as a **single-writer** with `private(set)` internal collections, external code cannot mutate state directly. All changes flow through the intent API, ensuring that reads and writes are atomic and that external observers receive updates only after the mutation completes.

### Why does BitChat use an intent API pattern?

The **intent API** (methods like `append(_:to:)` and `markRead(_:)`) encapsulates state transitions, allowing the store to enforce invariants such as ID index maintenance, message capping based on **TransportConfig**, and unread-count calculations. This pattern prevents scattered mutability, makes state changes auditable, and enables O(1) incremental updates instead of expensive full-collection rebuilds.

### Where are conversation messages actually stored?

Messages reside in the **Conversation** objects held by **ConversationStore**. Each `Conversation` maintains an in-memory array of messages (capped based on **TransportConfig** settings) and exposes them via `@Published` properties for SwiftUI observation. The store itself manages these objects in a dictionary keyed by conversation ID, providing the single point of access for the entire application.