# How the GossipMessageArchive Persists Public Messages in Bitchat

> Discover the GossipMessageArchive in bitchat. Learn how this JSON on-disk store ensures persistent public message storage for mesh participants, enabling message retrieval even after hours.

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

---

**`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/GossipMessageArchive.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipMessageArchive.swift#L12-L17):

```swift
/// 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`](https://github.com/permissionlesstech/bitchat/blob/main/sync/public-messages.json). The `defaultFileURL()` method constructs this path using Foundation's `FileManager`:

```swift
// 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:

```swift
// 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:

```swift
// 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](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipMessageArchive.swift#L50-L52):

```swift
#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:

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

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

```

[`ChatViewModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatViewModel.swift) invokes `GossipMessageArchive.wipeDefault()` for user-triggered security actions.

## Integration with GossipSyncManager

`BLEService` conditionally instantiates the archive and injects it into `GossipSyncManager`:

```swift
// 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

```swift
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

```swift
// 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/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/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/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/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/GossipSyncManager.swift) for the specific concurrency model in your Bitchat version.