# Bitchat Database Schema: In-Memory Architecture with ConversationStore

> Explore the bitchat database schema, featuring an in-memory architecture and ConversationStore for efficient data management. Learn how conversations and messages are stored.

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

---

**Bitchat does not use a traditional on-disk database; instead, it stores all persistent data in memory through a single source-of-truth component called `ConversationStore`, which manages `Conversation` objects containing ordered arrays of `BitchatMessage` instances.**

This article explores the complete database schema for [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat), a Swift-based mesh messaging application. Understanding this in-memory architecture reveals how the codebase achieves fast, observable state without SQL or Core Data dependencies.

---

## Core Schema Entities

The Bitchat database schema consists of four interconnected Swift types. No external schema files or migrations are required.

### ConversationStore

The **`ConversationStore`** in [`bitchat/App/ConversationStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/ConversationStore.swift) serves as the central writer and holder of all chat state. It maintains several lookup dictionaries for efficient access:

| Property | Purpose |
|----------|---------|
| `conversationIDs: [ConversationID]` | Ordered list of all conversation identifiers |
| `conversationsByID: [ConversationID: Conversation]` | Primary storage mapping IDs to conversation objects |
| `conversationIDsByMessageID: [String: Set<ConversationID>]` | Global reverse index from message ID to containing conversations |
| `selectedConversationID: ConversationID?` | UI-driven pointer to currently displayed conversation |
| `activeChannel: Channel?` | Currently active public channel for mesh/location chats |
| `selectedPrivatePeerID: PeerID?` | Currently selected peer for direct message view |

The store also enforces **capacity caps** through `TransportConfig.meshTimelineCap`, `geoTimelineCap`, and `privateChatCap` to prevent unbounded memory growth.

### Conversation

Each **`Conversation`** represents one timeline—whether mesh broadcast, geohash-based location chat, or direct peer-to-peer. Key characteristics:

- **Immutable for readers**: All mutations flow through the store's intent API
- **Observable updates**: Uses `@Published var messages: [BitchatMessage]` for SwiftUI reactivity
- **Fast lookup**: Maintains `indexByMessageID: [String: Int]` for O(1) message access

```swift
// From bitchat/App/ConversationStore.swift
class Conversation: ObservableObject {
    let id: ConversationID
    let cap: Int
    @Published var messages: [BitchatMessage]
    @Published var isUnread: Bool
    var indexByMessageID: [String: Int]  // message-ID → array index
}

```

### ConversationID

The **`ConversationID`** enum, defined in [`bitchat/App/AppArchitecture.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/AppArchitecture.swift) (lines 64-68), provides type-safe conversation identification:

```swift
enum ConversationID: Hashable, Codable {
    case mesh                    // Public mesh timeline
    case geohash(String)        // Location-based channel
    case direct(PeerHandle)     // Private peer-to-peer
}

```

This enum enables exhaustive switch statements and prevents invalid conversation types at compile time.

### BitchatMessage

The concrete message payload lives in [`localPackages/BitFoundation/Sources/BitFoundation/BitchatMessage.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/BitchatMessage.swift). Notably, it uses a **reference-type `class`** rather than `struct`, ensuring that delivery status updates propagate instantly to all observers:

| Property | Type | Description |
|----------|------|-------------|
| `id` | `String` | Unique message identifier |
| `sender` | `String` | Display name of originator |
| `content` | `String` | Message text payload |
| `timestamp` | `Date` | Creation time (used for ordering) |
| `isRelay` | `Bool` | Whether this is a forwarded message |
| `originalSender` | `String?` | For relayed messages, the true originator |
| `isPrivate` | `Bool` | Direct message flag |
| `recipientNickname` | `String?` | Target display name for private messages |
| `senderPeerID` | `PeerID?` | Cryptographic identity of sender |
| `mentions` | `[String]` | @-mentioned nicknames |
| `deliveryStatus` | `DeliveryStatus` | Acknowledgment tracking |
| `isBridged` | `Bool` | Whether message arrived via bridge node |

---

## Data Flow Operations

### Creating and Retrieving Conversations

The `ConversationStore.conversation(for:)` method lazily instantiates conversations with appropriate capacity limits:

```swift
let store = ConversationStore()
let meshConv = store.conversation(for: .mesh)           // Uses meshTimelineCap
let geoConv = store.conversation(for: .geohash("9q8y")) // Uses geoTimelineCap

```

### Message Insertion with Automatic Trimming

The `ConversationStore.append(_:to:)` → `Conversation.insert(_:)` chain maintains timestamp ordering and enforces capacity:

```swift
let message = BitchatMessage(
    sender: "alice",
    content: "Hello, mesh!",
    timestamp: Date(),
    isRelay: false,
    isPrivate: false
)

store.append(message, to: .mesh)

```

This operation:
1. Inserts in timestamp-sorted position
2. Updates `indexByMessageID`
3. Evicts oldest messages if count exceeds `cap`

### Delivery Status Updates

The schema supports **no-downgrade** delivery status through `Conversation.applyDeliveryStatus(_:forMessageID:)`:

```swift
let status = DeliveryStatus.delivered(to: "bob", at: Date())
store.setDeliveryStatus(status, forMessageID: messageID)

```

The store fans out updates to mirrored direct conversations automatically.

### Conversation Migration

Direct conversations support identity migration via `migrateConversation(from:to:)`:

```swift
let oldID = ConversationID.directPeer(temporaryPeer)
let newID = ConversationID.directPeer(verifiedPeer)
store.migrateConversation(from: oldID, to: newID)

```

---

## Schema Integrity Guarantees

### In-Memory Audit System

`ConversationStore.auditInvariants()` provides database-like consistency checks without persistence overhead. This method validates:

- ID-index consistency across all conversations
- Strict timestamp ordering within each timeline
- Capacity enforcement
- Global message-ID → conversation mapping correctness

The audit runs in the test suite to catch schema violations during development.

### Observable State Contract

The `@Published` property wrappers on `Conversation.messages` and `Conversation.isUnread` create a reactive data layer. UI components in [`bitchat/Views/MessageListView.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Views/MessageListView.swift) observe these directly, eliminating need for separate data binding infrastructure.

---

## Key Source Files

| File | Purpose |
|------|---------|
| [`bitchat/App/ConversationStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/ConversationStore.swift) | Complete in-memory schema implementation, intent API, and audit logic |
| [`bitchat/App/AppArchitecture.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/AppArchitecture.swift) | `ConversationID` enum and UI selection types |
| [`localPackages/BitFoundation/Sources/BitFoundation/BitchatMessage.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/BitchatMessage.swift) | Message model with binary coding |
| [`docs/CONVERSATION-STORE-DESIGN.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/CONVERSATION-STORE-DESIGN.md) | Design rationale for in-memory approach |

---

## Summary

- **Bitchat's database schema is entirely in-memory** through `ConversationStore`—no SQLite, Core Data, or disk persistence
- **Four core types** form the schema: `ConversationStore`, `Conversation`, `ConversationID`, and `BitchatMessage`
- **Reference semantics** on `BitchatMessage` enable instant delivery status propagation
- **Built-in capacity management** prevents memory exhaustion via configurable caps
- **Observable design** replaces traditional query patterns with SwiftUI-native reactivity
- **Runtime audits** verify schema invariants without formal database constraints

---

## Frequently Asked Questions

### How does Bitchat persist data across app restarts?

It doesn't. The codebase intentionally eschews on-disk persistence for simplicity and privacy. Messages exist only in memory during the app session. This design choice is documented in [`docs/CONVERSATION-STORE-DESIGN.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/CONVERSATION-STORE-DESIGN.md) as a deliberate trade-off for a lightweight, permissionless messaging system.

### Why is BitchatMessage a class instead of a struct?

Reference semantics allow delivery status updates to propagate instantly to all UI observers. If `BitchatMessage` were a value type, every status change would require replacing the entire message array and rebuilding indexes. The class design enables surgical mutations while maintaining `@Published` reactivity.

### What prevents unbounded memory growth in ConversationStore?

Three mechanisms enforce limits: per-conversation `cap` properties (`meshTimelineCap`, `geoTimelineCap`, `privateChatCap`), automatic oldest-message eviction during `insert(_:)`, and the `auditInvariants()` test suite verification. These bounds are configurable through `TransportConfig`.

### Can I query messages by content or sender?

Not directly through the store API. The schema optimizes for ID-based and temporal access via `indexByMessageID` and sorted arrays. Content filtering would require linear scan or building a secondary index in application code. The `conversationIDsByMessageID` map only supports exact message ID lookups, not content search.