Bitchat Database Schema: In-Memory Architecture with ConversationStore

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, 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 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
// 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 (lines 64-68), provides type-safe conversation identification:

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. 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:

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:

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:):

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:):

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 observe these directly, eliminating need for separate data binding infrastructure.


Key Source Files

File Purpose
bitchat/App/ConversationStore.swift Complete in-memory schema implementation, intent API, and audit logic
bitchat/App/AppArchitecture.swift ConversationID enum and UI selection types
localPackages/BitFoundation/Sources/BitFoundation/BitchatMessage.swift Message model with binary coding
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 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.

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 →