How the GossipMessageArchive Persists Public Messages in Bitchat

GossipMessageArchive is a lightweight JSON-based on-disk store that preserves signed public message packets across app restarts and device migrations, enabling mesh participants to retrieve missed broadcasts even hours later.

The archive sits at the heart of Bitchat's gossip synchronization layer. When a node receives or broadcasts public messages, GossipSyncManager relies on this component to maintain durable state—effectively turning any phone into a "town crier" that can walk between network partitions or relaunch and still supply historical messages to new peers.

What Is the GossipMessageArchive?

GossipMessageArchive provides disk persistence for the gossip-sync public message store. Unlike encrypted private messages, public broadcasts are already signed and verifiable by any participant. The archive therefore focuses purely on durability rather than confidentiality, storing raw binary packets in a JSON-encoded array.

According to the [source documentation in GossipMessageArchive.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipMessageArchive.swift#L12-L17):

/// Disk persistence for the gossip-sync public message store.
/// Holds raw binary packets (already signed and broadcast) that
/// represent public messages. Survives app restarts and device
/// migrations so the recent public-message history remains
/// available for gossip synchronization.

This design choice reflects Bitchat's core philosophy: authenticated public data can be safely persisted without encryption, while the signatures ensure integrity regardless of storage location.

File Location and Storage Path

The archive lives in the app's Application Support directory at sync/public-messages.json. The defaultFileURL() method constructs this path using Foundation's FileManager:

// From GossipMessageArchive.swift, lines 69-78
private static func defaultFileURL() -> URL {
    let fm = FileManager.default
    let appSupport = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
    let syncDir = appSupport.appendingPathComponent("sync", isDirectory: true)
    
    // Create directory if needed
    try? fm.createDirectory(at: syncDir, withIntermediateDirectories: true)
    
    return syncDir.appendingPathComponent("public-messages.json")
}

This location is appropriate for app-specific data that persists across updates but can be recreated if lost.

JSON Encoding and Data Structure

The archive stores messages as a JSON-encoded array of Data blobs. Each blob represents one complete, signed public packet ready for broadcast or re-transmission.

Loading Persisted Messages

The load() method decodes the stored array or returns empty if the file is absent:

// From GossipMessageArchive.swift, lines 28-34
func load() -> [Data] {
    guard FileManager.default.fileExists(atPath: fileURL.path) else {
        return []  // Graceful fallback for first launch
    }
    let data = try! Data(contentsOf: fileURL)
    return try! JSONDecoder().decode([Data].self, from: data)
}

Saving Message Batches

The save() method atomically writes the packet array, with platform-specific protection:

// From GossipMessageArchive.swift, lines 37-53
func save(_ packets: [Data]) {
    // Remove file if empty to avoid leaving stale data
    guard !packets.isEmpty else {
        try? FileManager.default.removeItem(at: fileURL)
        return
    }
    
    let data = try! JSONEncoder().encode(packets)
    
    var options: Data.WritingOptions = []
    #if os(iOS)
    options.insert(.completeFileProtection)  // Encrypt when device locked
    #endif
    
    try! data.write(to: fileURL, options: options)
}

Platform-Specific File Protection

On iOS, the archive uses .completeFileProtection to encrypt the file when the device is locked. This adds defense-in-depth even though the packets contain public data. On macOS and other platforms, standard write options suffice because the signatures already provide authenticity.

As implemented in lines 50-52:

#if os(iOS)
options.insert(.completeFileProtection)
#endif

Lifecycle and Cleanup Operations

The archive supports three lifecycle states:

State Behavior Method
Empty/missing Returns [] without error load()
Active JSON file with packet array save(packets)
Wiped File deleted entirely wipe() or wipeDefault()

The wipe() instance method and static wipeDefault() helper enable panic-wipe scenarios where any component can erase persisted state without holding a live archive reference:

// From GossipMessageArchive.swift, lines 59-66
func wipe() {
    try? FileManager.default.removeItem(at: fileURL)
}

static func wipeDefault() {
    GossipMessageArchive().wipe()
}

ChatViewModel.swift invokes GossipMessageArchive.wipeDefault() for user-triggered security actions.

Integration with GossipSyncManager

BLEService conditionally instantiates the archive and injects it into GossipSyncManager:

// From BLEService.swift, lines 689-690
let archive = meshBackgroundEnabled ? GossipMessageArchive() : nil
let gossipManager = GossipSyncManager(..., archive: archive)

This integration pattern means:

  • Background mesh enabled: Full persistence across restarts
  • Background mesh disabled: Ephemeral operation, no disk storage

GossipSyncManager calls load() on startup to populate its in-memory cache, then save() after ingesting new packets from peers.

Practical Code Examples

Basic Archive Operations

import Foundation

// 1️⃣ Create (or retrieve) the default archive
let archive = GossipMessageArchive()               // uses defaultFileURL()

// 2️⃣ Load persisted public messages at startup
let storedPackets: [Data] = archive.load()        // → [] if file absent

// 3️⃣ Append a newly-received public packet and persist
var allPackets = storedPackets
allPackets.append(newPublicPacket)                 // `newPublicPacket` is Data
archive.save(allPackets)                          // writes JSON atomically

// 4️⃣ Remove the entire archive (e.g., on user "panic")
GossipMessageArchive.wipeDefault()                // deletes the file

Working with GossipSyncManager

// Typical initialization flow in BLEService
if meshBackgroundEnabled {
    let archive = GossipMessageArchive()
    let gossipManager = GossipSyncManager(
        identity: myIdentity,
        archive: archive
    )
    
    // GossipManager internally calls archive.load() to recover state
    // and archive.save(updatedPackets) after each sync batch
}

Key Source Files

File Role
[GossipMessageArchive.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipMessageArchive.swift) Core persistence implementation: load, save, wipe operations
[GossipSyncManager.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipSyncManager.swift) Coordinates gossip sync; owns archive lifecycle
[BLEService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift) Instantiates archive based on background mesh setting
[ChatViewModel.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatViewModel.swift) Triggers panic-wipe via static helper

Summary

  • GossipMessageArchive provides durable storage for signed public message packets in Bitchat's mesh network
  • Storage format is JSON-encoded [Data] array at Application Support/sync/public-messages.json
  • iOS file protection encrypts the archive when device is locked; other platforms rely on packet signatures
  • Graceful degradation: missing file returns empty array, empty save removes file entirely
  • Panic-wipe capable: static wipeDefault() enables security-critical cleanup from any context
  • Integration with GossipSyncManager enables survivable gossip state across app restarts and network partitions

Frequently Asked Questions

What happens if the public-messages.json file is corrupted?

GossipMessageArchive.load() uses try! with JSONDecoder, meaning a corrupted file would crash the app in debug builds. In production Swift, this triggers a runtime exception. The current implementation assumes well-formed JSON; for resilience, you could wrap decoding in a do-catch and return [] on failure.

Can I migrate the GossipMessageArchive to a different device?

Yes. Since the archive contains only signed public packets with no device-specific encryption, you can transfer public-messages.json between devices. The receiving device will verify signatures normally using PublicMessage.verify(). This property enables "town crier" scenarios where one device carries message history to another network segment.

Why not use Core Data or SQLite for this storage?

JSON with Data blobs provides simpler atomic writes and zero migration complexity for a single append-only array. The access pattern is bulk load/save rather than query-heavy, making JSON appropriate. If message volume grew substantially or querying became necessary, SQLite would become justified.

How does the archive handle concurrent writes?

data.write(to:options:) with default Data.WritingOptions provides atomic replacement on filesystems that support it. However, GossipSyncManager currently serializes access through actor isolation or main-thread coordination—see GossipSyncManager.swift for the specific concurrency model in your Bitchat version.

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 →