# What Data Does BitChat Store and Where? Privacy‑First Storage Architecture Explained

> Discover what data BitChat stores and where. Learn about its privacy-first architecture, keeping minimal data locally and conversation histories in memory for enhanced security.

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

---

**BitChat stores only minimal, bounded data locally—cryptographic secrets in the iOS Keychain, preferences in UserDefaults, and temporary encrypted payloads in Application Support—while keeping full conversation histories purely in memory.**

BitChat is an open‑source, peer‑to‑peer messaging app built by permissionlesstech with a "privacy‑first" design philosophy. Unlike conventional chat apps that persist entire message histories to disk, BitChat's architecture intentionally limits what data gets stored and where. This article breaks down every data category, its physical storage location, and the security mechanisms protecting it, based on the actual source code in the bitchat repository.

## Keychain Secrets: Device‑Only Cryptographic Keys

BitChat stores all cryptographic secrets exclusively in the **iOS Keychain** with device‑only accessibility. These keys never leave the device and cannot be backed up or transferred.

| Secret Type | Purpose | Keychain Tag |
|-------------|---------|--------------|
| Noise static key | Long‑term identity for Noise protocol handshakes | Derived from device entropy |
| Group keys | Symmetric keys for encrypted group messages | Per‑group entry |
| Outbox encryption key | ChaChaPoly key for sealing pending private messages | `"outboxEncryptionKey"` |
| Geohash seed | Root seed for deriving per‑location Nostr identities | `"geohashSeed"` |

According to the privacy assessment in [`docs/privacy-assessment.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/privacy-assessment.md) (lines 41‑46), these items are configured with **kSecAttrAccessibleWhenUnlockedThisDeviceOnly**, meaning they are inaccessible when the device is locked and cannot migrate to a new device via iCloud backup.

## User Preferences and Metadata

Non‑secret user data lives in **UserDefaults** within the app container. The repository includes `bitchat/PrivacyInfo.xcprivacy` (lines 87‑90) declaring this usage to App Store review.

Stored preferences include:

- Nickname and display name
- Selected geohash channels and bookmarked locations
- Favorites and petnames for contacts
- Read‑receipt identifiers
- App settings (notifications, themes, etc.)

**Important**: [`LocationPresenceStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/LocationPresenceStore.swift) handles geohash selection and teleport flags, but this state is not persisted beyond the app sandbox—no server‑side location history exists.

## Group State in Protected JSON Files

Group metadata—names, roster, creator identity, epoch, and group keys—is written to **protected JSON files** in `~/Library/Application Support/`.

The files are protected by the OS file system, and the cryptographic group keys referenced within are themselves stored in the Keychain. This split design ensures that compromising the JSON file alone does not expose message content.

## Outbound Private Outbox: Encrypted, Time‑Bounded

Pending private messages awaiting acknowledgment are **ChaChaPoly‑sealed** and written to:

```

…/Application Support/Outbox/

```

- **Encryption**: ChaChaPoly with key from Keychain
- **Time‑to‑live**: 24 hours maximum
- **Cleanup**: Automatic eviction on panic wipe or TTL expiration

The `enqueuePrivateMessage` function in the codebase (shown below) demonstrates this flow:

```swift
import Foundation
import Security

func enqueuePrivateMessage(_ data: Data) throws {
    // Encrypt with a ChaChaPoly key stored in the keychain
    let key = try KeychainHelper.retrieveKey(tag: "outboxEncryptionKey")
    let sealed = try ChaChaPoly.seal(data, using: key)
    let outboxURL = FileManager.default.urls(
        for: .applicationSupportDirectory, in: .userDomainMask
    )[0].appendingPathComponent("Outbox")
    try FileHelper.append(sealed.ciphertext, to: outboxURL)
}

```

## Courier Mail: Opaque Envelopes for Store‑and‑Forward

Devices acting as **couriers** temporarily hold Noise‑sealed envelopes for offline recipients. These are stored in the same Application Support location as the outbox with identical protection:

- Encrypted (courier cannot read contents)
- 24‑hour TTL
- Cleared on panic wipe

The courier implementation ensures that even when a device relays messages for others, it gains no access to plaintext content.

## Public Gossip Archive: Temporary Mesh History

Public mesh messages—signed gossip used for network discovery—are stored as **plain files** in Application Support with strict ephemerality:

- **Maximum retention**: 6 hours
- **Automatic removal**: On app restart or panic wipe
- **Storage format**: Raw signed messages, no encryption needed (public by design)

## Public Board Posts and Tombstones: Bounded Archives

Author‑signed public board events persist for up to **7 days** (author‑selected expiry). These include both posts and tombstones (deletion markers).

Storage characteristics:

| Attribute | Value |
|-----------|-------|
| Location | `…/Application Support/BoardArchive/` |
| Format | Plain files (signed, self‑authenticating) |
| Quota | Bounded by configurable limit |
| Cleanup | Oldest‑first eviction, panic wipe clears all |

## Media Files: Voice Notes and Images

Received and sent media is stored in a dedicated subtree:

```

…/Application Support/Media/

```

| Limit | Policy |
|-------|--------|
| **Size quota** | 100 MB maximum |
| **Age limit** | 7 days per file |
| **Eviction** | Oldest‑first when quota exceeded |
| **Emergency** | Entire tree deleted on panic wipe |

The `loadMediaIfValid` helper enforces the age limit at access time:

```swift
func loadMediaIfValid(at url: URL) -> Data? {
    guard let attrs = try? FileManager.default.attributesOfItem(atPath: url.path),
          let modDate = attrs[.modificationDate] as? Date else { return nil }
    // Discard files older than 7 days
    guard Date().timeIntervalSince(modDate) < 7 * 24 * 60 * 60 else { return nil }
    return try? Data(contentsOf: url)
}

```

## Location State and Geohash Identities

BitChat implements **location‑based pseudonymity** through per‑geohash Nostr identities. The architecture splits this across two stores:

- **Keychain**: Stores the single root seed (`"geohashSeed"`)
- **UserDefaults**: Caches derived identity strings for active geohashes

Derived secp256k1 keys are regenerated from seed + geohash as needed:

```swift
func nostrIdentity(for geohash: String) throws -> SecKey {
    let seed = try KeychainHelper.retrieveData(tag: "geohashSeed")
    // Derive a secp256k1 key from the seed + geohash
    return try CryptoHelper.deriveNostrKey(seed: seed, salt: geohash)
}

```

This design ensures that changing geohashes produces unlinkable identities without requiring persistent storage of multiple private keys.

## In‑Memory Conversation Store: True Ephemerality

The most significant architectural decision: **full chat histories never touch disk**.

[`bitchat/App/ConversationStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/ConversationStore.swift) maintains the complete timeline of public and private conversations purely in RAM. When the app terminates or is killed by the system, this data is lost permanently. The next launch reconstructs only what can be fetched from the mesh network in real time.

As documented in `privacy‑assessment.md` (lines 165‑166), this is **intentional by design** to eliminate forensic recovery risks.

## Panic Wipe: Emergency Data Destruction

BitChat implements a **triple‑tap emergency wipe** (documented in `privacy‑assessment.md`, lines 10‑12). When triggered:

1. All Application Support files deleted
2. Media tree purged
3. Keychain items removed
4. UserDefaults cleared
5. In‑memory stores dropped

This provides cryptographic erasure of local data under duress without requiring network connectivity.

## Core Source Files

| File | Responsibility |
|------|--------------|
| [`bitchat/App/ConversationStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/ConversationStore.swift) | In‑memory chat history backing UI models |
| [`bitchat/App/LocationPresenceStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/LocationPresenceStore.swift) | Geohash selection and presence state |
| [`bitchat/Identity/SecureIdentityStateManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Identity/SecureIdentityStateManager.swift) | Keychain‑backed identity lifecycle |
| [`bitchat/Noise/NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Noise/NoiseSessionManager.swift) | Ephemeral Noise session state (memory‑only) |
| [`docs/privacy-assessment.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/privacy-assessment.md) | Authoritative enumeration of all storage |
| [`PRIVACY_POLICY.md`](https://github.com/permissionlesstech/bitchat/blob/main/PRIVACY_POLICY.md) | User‑facing data practices summary |

## Summary

- **Cryptographic secrets**: iOS Keychain, device‑only accessibility, never exported
- **Preferences/metadata**: UserDefaults, declared in `PrivacyInfo.xcprivacy`
- **Encrypted payloads**: Application Support with 24‑hour TTL
- **Public archives**: Plain files, 6‑hour to 7‑day bounded retention
- **Media**: 100 MB / 7‑day quota in Application Support
- **Conversations**: **Never persisted**—purely in‑memory via [`ConversationStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ConversationStore.swift)
- **Emergency wipe**: Triple‑tap triggers complete local data destruction

## Frequently Asked Questions

### Does BitChat store messages on a server?

No. BitChat is peer‑to‑peer only. Messages are either delivered directly via the mesh or temporarily stored by courier devices—never on centralized infrastructure. The courier storage is encrypted and time‑bounded to 24 hours.

### Can law enforcement recover deleted BitChat messages?

Forensic recovery of message content is extremely difficult. Full conversation histories reside only in RAM and are lost on app termination. Persistent stores are encrypted or plain public data with short TTLs. The panic wipe explicitly deletes all recoverable data and Keychain items.

### Why does BitChat use UserDefaults for some data instead of Keychain?

UserDefaults is appropriate for non‑secret configuration data (nicknames, settings) that presents no security risk if extracted. Cryptographic material—keys, seeds, encryption secrets—always uses Keychain with device‑only accessibility. This distinction is enforced throughout [`SecureIdentityStateManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/SecureIdentityStateManager.swift).

### How does BitChat handle media compared to text messages?

Media follows **strict quota and age limits** (100 MB, 7 days) while text messages in the conversation timeline are **never written to disk**. This asymmetric design reflects the higher storage cost and lower sensitivity of media files versus message content.