How BitChat Handles Persistence for Board Posts and Public Messages During Network Partitions

BitChat uses an immutable, signed-packet store that persists board posts and public messages to disk via the BoardStore singleton, automatically reloading them on launch to survive network partitions and re-synchronizing via gossip when connectivity returns.

BitChat, an open-source mesh messaging application from the permissionlesstech/bitchat repository, guarantees message durability across network outages by treating posts as immutable cryptographic packets written to local storage. The persistence layer ensures that both bulletin-board notices and public chat messages survive temporary partitions and seamlessly reconcile when peers reconnect.

Board Post Persistence Architecture

On-Disk Storage Format

In bitchat/Services/Board/BoardStore.swift, the store saves each raw signed packet exactly as received (BitchatPacket) alongside a retention deadline for tombstones. This design preserves cryptographic authenticity while providing explicit expiration controls for resource management.

The BoardStore Singleton

BoardStore operates as a singleton via static let shared, maintaining live posts and tombstones in memory while mirroring changes to disk. When a new packet is accepted, the store immediately persists the updated state, ensuring that data survives app termination or device restarts without corruption.

Ingestion and Validation Pipeline

The BoardStore.ingest(_:, packet:) method in bitchat/Services/Board/BoardStore.swift handles the validation pipeline:

  1. Decodes the packet into a BoardWire enum (either .post or .tombstone).
  2. Validates the packet signature, checks expiration times, enforces clock-skew limits, and applies duplicate-detection rules.
  3. Upon acceptance, calls persistLocked() to write to disk and emits postArrivals on the main thread for UI updates.

Retention Rules and Quota Enforcement

The store enforces strict resource limits to ensure devices with constrained storage can still participate in the gossip protocol:

  • Post expiry: Posts are retained until the author-specified expiration, capped at 7 days.
  • Tombstone retention: Tombstones persist until the original post's expiry, or for an orphanTombstoneLifetimeMs window if the post was never received.
  • Storage quotas: Each author is limited to 5 posts, with a global cap of 200 total posts. When limits are exceeded, the oldest entries are evicted.

Public Message Persistence

Public chat messages follow the identical persistence pattern but utilize the generic GossipMessageArchive defined in bitchat/Models/GossipMessageArchive.swift. Each signed chat packet is stored as raw Data with an optional retain-until timestamp, allowing the ChatTransport layer to retrieve synchronization candidates via syncCandidates(). This unified approach ensures that public messages survive partitions using the same immutable packet store as board posts.

Recovery After Network Partitions

When the app launches, loadFromDisk() (called from the BoardStore initializer) re-ingests all persisted packets from bitchat/Services/Board/BoardStore.swift, rebuilding the in-memory state from the raw signed data. Because packets are stored unchanged, any messages missed during a partition are automatically offered to peers during the next gossip synchronization cycle managed by GossipSyncManager.swift. This mechanism ensures eventual consistency across the mesh network without requiring complex conflict resolution algorithms.

Implementation Examples

The following Swift patterns demonstrate how BitChat interacts with the persistence layer during normal operation and synchronization:

// Ingest a board post (called from the radio or sync handler)
let result = BoardStore.shared.ingest(wire, packet: incomingPacket)
if result == .accepted {
    // UI will be notified via `postArrivals`
}

// Retrieve posts for a specific geohash (e.g. local area)
let localPosts = BoardStore.shared.posts(forGeohash: myGeohash)

// Get all packets that should be sent in the next gossip round
let gossipCandidates = BoardStore.shared.syncCandidates()

Summary

  • Immutable storage: Both board posts and public messages are stored as raw signed packets (BitchatPacket) in BoardStore.swift to preserve cryptographic authenticity.
  • Singleton pattern: BoardStore.shared manages in-memory state and disk persistence via persistLocked() and loadFromDisk().
  • Strict validation: The ingest() method filters expired, duplicate, or clock-skewed packets before storage.
  • Resource limits: Hard caps of 5 posts per author and 200 total posts prevent storage exhaustion on resource-constrained devices.
  • Automatic recovery: On launch, loadFromDisk() restores state, and syncCandidates() makes missed messages available for gossip reconciliation once connectivity returns.

Frequently Asked Questions

How does BitChat ensure messages aren't lost when the network goes down?

BitChat writes every accepted packet to disk immediately via BoardStore.persistLocked(), maintaining the raw signed data in bitchat/Services/Board/BoardStore.swift. When the app restarts after a partition, loadFromDisk() reconstructs the in-memory state from these immutable packets, ensuring no data is lost due to connectivity issues.

What limits prevent the board storage from growing indefinitely?

The implementation enforces two quota rules: a maximum of 5 posts per author and a global limit of 200 total posts. When these caps are reached, the oldest entries are automatically evicted during the ingestion process, as implemented in the persistence logic of BoardStore.swift.

How are deleted posts handled during a network partition?

When a post is deleted, a tombstone record is created that persists until the original post's expiration date or for a defined orphan window (orphanTombstoneLifetimeMs). These tombstones are stored alongside posts in BoardStore and re-broadcast after partitions to notify peers of deletions even if they missed the original content.

Can public chat messages use the same persistence mechanism as board posts?

Yes, public messages utilize the same architectural pattern through GossipMessageArchive, storing raw signed packets that are retrieved via syncCandidates() and synchronized through the ChatTransport layer when connectivity resumes, ensuring consistent persistence across both message types.

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 →