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:
- Inserts in timestamp-sorted position
- Updates
indexByMessageID - 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, andBitchatMessage - Reference semantics on
BitchatMessageenable 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →