Guides
Storage & Data Sovereignty
Local-first is a security property, an availability property, and a political statement: your history lives on your hardware, encrypted under your keys, exportable at will, and deletable for real.
1. Principles
- The device is the source of truth. No cloud copy exists unless the user creates an encrypted export. Sync between own devices (M6) is device-to-device, E2E-encrypted.
- Everything at rest is sealed. No plaintext ever touches disk — including drafts, media thumbnails, and search indexes.
- Export is a right. Full history exports to a documented, versioned format at any time. Lock-in is a bug.
- Deletion is real. Deleting a message deletes the ciphertext row and its keys; retention policies (per-conversation disappearing messages) are enforced locally. We are honest that the recipient's copy is theirs — no fake "remote delete" theater beyond a polite delete-request the peer may honor.
2. Layout
SQLite database, accessed only through kult-store. Key hierarchy per
04 — Cryptography §8: Argon2id-derived KEK → master key SK →
HKDF per-domain keys.
| Domain | Contents | Notes |
|---|---|---|
identity |
Own keys (wrapped), device settings | Smallest, most sensitive; extra wrap layer |
sessions |
Serialized ratchet states, skipped-key store | Rewrapped on every persist; zeroized in memory after |
contacts |
Peer keys, verification state, petnames, relay hints | Never leaves the device |
messages |
Envelope plaintexts post-decrypt, delivery state | Per-blob AEAD, random nonces |
queue |
Outbound envelopes pending delivery per transport | Ciphertext only — survives crash/restart |
prekeys |
Own signed/PQ/one-time prekey secrets | One-time prekeys deleted on use |
pending |
Inbound envelopes not yet readable (arrived before their session) | Ciphertext only; TTL-bounded |
media |
Attachment blobs, chunked | Each chunk sealed; keys stored in messages |
Every blob is individually AEAD-sealed (XChaCha20-Poly1305, random 24-byte nonce, table name + row purpose as associated data) — a copied database file leaks only row counts and approximate sizes; rows can't be transplanted across tables or databases.
3. Search
Full-text search runs over a sealed local index: tokenized terms are HMAC'd under a search-domain key before insertion, so the index file leaks no vocabulary. Query = HMAC the query terms, look up. (Trades fuzzy matching for sealed storage — the right trade for this project.)
4. Backup & portability
- Encrypted backup file: single-file export (identity + contacts + history + session-reset markers), sealed under a key derived from a BIP-39-style mnemonic via Argon2id. Restoring on a new device resumes identity; sessions re-handshake (ratchet states are deliberately not portable — importing stale ratchet state is a correctness and security hazard).
- Plaintext export: JSON-lines + media directory, clearly warned as plaintext. The user's data is the user's.
- Panic wipe (roadmap M6): duress passphrase unlocking a decoy profile while destroying the real KEK wrap — recorded here so the key hierarchy keeps the KEK-wrap layer that makes O(1) destruction possible.
5. What never gets stored
- Plaintext of anything, anywhere, ever (including logs — log lines are structured and content-free by policy, enforced by a lint in CI).
- Message keys after use; chain keys after advancing (zeroize-on-drop).
- Contact graphs on any remote system. Relay queues hold only sealed envelopes under rotating tokens with TTLs.