What Data Does BitChat Store and Where? A Privacy‑First Storage Architecture
BitChat stores only minimal, bounded data on device: cryptographic secrets in the system keychain, preferences in UserDefaults, and encrypted/transient payloads in Application Support, while full conversation history remains purely in memory.
The open-source BitChat messaging app built by permissionlesstech adopts a "privacy-first" ephemerality model. According to the source code analyzed in docs/privacy-assessment.md, most user conversation data never touches persistent storage. Only specific categories—encrypted outbox messages, temporary public archives, and media files with strict quotas—are written to disk, all protected by platform security mechanisms and automatic expiration.
Key Data Types and Their Storage Locations
BitChat organizes persistent data into three tiers: keychain-backed secrets, UserDefaults preferences, and file-based encrypted/transient stores in Application Support.
Cryptographic Secrets: System Keychain
All cryptographic material uses device-only keychain accessibility. This includes:
- Noise static keys for encrypted sessions
- Group encryption keys for private group messaging
- Outbox encryption keys for pending private messages
- Per-geohash Nostr seeds for location-based identities
These items are configured with kSecAttrAccessibleWhenUnlockedThisDeviceOnly, meaning they cannot be transferred to another device or accessed when locked. On a panic wipe—triggered by a triple-tap emergency gesture—the app deletes all keychain entries alongside file-based stores.
User Preferences and Metadata: UserDefaults
Lightweight settings persist in UserDefaults within the app sandbox:
- Nickname and display preferences
- Favorites, petnames, and bookmarked locations
- Read-receipt identifiers
- Selected geohash channels and teleport flags
As declared in bitchat/PrivacyInfo.xcprivacy, this data usage is explicitly surfaced to App Store review processes.
Encrypted and Transient File Stores: Application Support
| Store | Contents | Lifetime | Protection |
|---|---|---|---|
| Outbound Private Outbox | ChaChaPoly-sealed messages awaiting delivery | 24 hour TTL | Encryption key in keychain; cleared on panic wipe |
| Courier Mail | Noise-sealed envelopes held by relay devices | 24 hour TTL | Same as outbox; courier cannot decrypt contents |
| Group State | Group names, roster, creator identity, epoch | Persistent | OS file protection + keychain-backed keys |
| Public Gossip Archive | Signed mesh messages | ≤6 hours | Removed on app restart or panic wipe |
| Public Board Posts | Signed board events with tombstones | Max 7 days | Bounded by quota; cleared on panic wipe |
| Media Files | Voice notes and images | Max 7 days | 100 MB quota with oldest-first eviction |
All file-based stores reside in ~/Library/Application Support/ subdirectories. The media tree at …/Application Support/Media/ enforces automatic cleanup: files exceed 7 days or the quota triggers deletion.
Ephemeral by Design: In-Memory Conversation Store
The defining architectural decision appears in bitchat/App/ConversationStore.swift: full chat histories live only in RAM. When the app terminates or the device reboots, this data disappears entirely. This eliminates forensic recovery risks for conversation content, as no plaintext messages are ever written to persistent storage.
The NoiseSessionManager.swift module reinforces this—Noise session keys and session state remain in-memory only, never serialized to disk.
Practical Code Examples
These Swift snippets from the source demonstrate store access patterns:
Enqueue an Encrypted Outbox Message (24h TTL)
import Foundation
func enqueuePrivateMessage(_ data: Data) throws {
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)
}
The KeychainHelper type resides in bitchat/Identity/, while FileHelper implements bounded-age appending logic.
Load Media with 7-Day Age Validation
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 }
guard Date().timeIntervalSince(modDate) < 7 * 24 * 60 * 60 else { return nil }
return try? Data(contentsOf: url)
}
Derive Location-Based Nostr Identity
func nostrIdentity(for geohash: String) throws -> SecKey {
let seed = try KeychainHelper.retrieveData(tag: "geohashSeed")
return try CryptoHelper.deriveNostrKey(seed: seed, salt: geohash)
}
The seed persists in keychain; derived identities regenerate on demand and do not require separate storage.
Core Architectural Guarantees
Four principles govern what data BitChat stores and where:
- Ephemerality First: Conversation content stays in memory; persistent stores are strictly bounded exceptions
- Time-Boxed Persistence: Every file-based store has hard limits (24 hours, 7 days, or quota-based eviction)
- Panic Wipe Capability: Triple-tap emergency wipe clears all persistent stores plus keychain secrets
- Minimal Attack Surface: Device compromise reveals only quota-limited, encrypted, or transient data—not full message history
Key Source Files
| File | Purpose |
|---|---|
docs/privacy-assessment.md |
Complete enumeration of persistent stores and security analysis |
bitchat/App/ConversationStore.swift |
In-memory conversation timeline implementation |
bitchat/App/LocationPresenceStore.swift |
Geohash and presence flag persistence |
bitchat/Identity/SecureIdentityStateManager.swift |
Keychain-backed identity lifecycle |
bitchat/Noise/NoiseSessionManager.swift |
In-memory Noise session state |
PRIVACY_POLICY.md |
User-facing data handling summary |
Summary
- BitChat stores cryptographic secrets exclusively in the system keychain with device-only accessibility
- UserDefaults holds lightweight preferences and location state, declared via
PrivacyInfo.xcprivacy - Application Support files contain encrypted outbox messages (24h), transient public archives (≤6h), board posts (7d max), and media (100MB quota, 7d max)
- Full conversation histories remain purely in-memory and vanish on app termination
- Panic wipe provides instant, complete data destruction across all stores
Frequently Asked Questions
Does BitChat store my chat messages on disk?
No. Full conversation timelines remain purely in-memory via ConversationStore.swift and are lost when the app terminates. Only encrypted outbox messages awaiting delivery are temporarily stored, sealed with ChaChaPoly and purged after 24 hours or upon panic wipe.
What happens to my data during a panic wipe?
A triple-tap emergency gesture triggers deletion of all keychain secrets, removes the entire media tree, clears all Application Support files, and erases UserDefaults. This leaves no recoverable cryptographic material or message content on the device.
How are my encryption keys protected?
All private keys—including Noise static keys, group keys, outbox encryption keys, and per-geohash Nostr seeds—reside in the iOS system keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly. They cannot be backed up or transferred to another device and are inaccessible when the device is locked.
Will media I receive remain on my device indefinitely?
No. Incoming voice notes and images are subject to a 100 MB quota with oldest-first eviction, plus a hard 7-day age limit. FileHelper automatically validates modification timestamps and rejects expired files during access attempts.
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 →