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 locally deletable within explicit endpoint and copy-retention limits.
1. Principles
- Owned devices are the source of truth. There is no authoritative provider-hosted history database. Optional mailbox and delayed-delivery carriers may temporarily retain bounded sealed envelopes, and users may create encrypted exports, but neither becomes readable message history for an operator. Implemented C2 sync is explicit device-to-device, end-to-end encrypted, and accepted only between account-authorized physical devices.
- Durable records and sensitive equality keys at rest are sealed. The core database does not persist message, draft, or media plaintext. Contact, group, delivery-token, message, media, device, and other guessable equality identifiers appear only inside row ciphertext or as database- and domain-separated keyed indexes. SQLite still reveals static schema structure, approximate row counts and sizes, insertion order, and access patterns. Bounded protected transients required by OS picker, recording, editing, playback, or explicit export workflows are temporary exceptions with lifecycle cleanup, never durable sources of truth.
- Export is a right. Full history exports to a documented, versioned format at any time. Lock-in is a bug.
- Local deletion has a precise boundary. Deleting a message removes the live logical row and application references from that Komms history; retention policies (per-conversation disappearing messages) are enforced on that endpoint. SQLite cleanup can reduce remnants but does not establish per-record cryptographic or forensic erasure. Komms cannot erase a recipient's copy, screenshot, export, operating-system artifact, previously copied backup, carrier ciphertext, filesystem snapshot, or data recovered from a compromised endpoint. A deletion hint is a request another endpoint may honor, not proof of remote erasure.
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 | Body sealed and rewrapped on persist; peer lookup is a keyed opaque locator |
contacts |
Peer keys, verification state, petnames, relay hints | Body sealed; peer lookup is a keyed opaque locator; selected fields may enter authenticated own-device sync |
devices |
Own and contact device certificates, signed manifests, revocation tombstones | Bounded authority state; exact physical identities |
device_sync |
Per-device channels, counters, Lamport winners, terminal convergence tombstones | Direction-bound, replay-protected, never a cloud log |
messages |
Envelope plaintexts post-decrypt, delivery state | Per-blob AEAD, random nonces |
queue |
Outbound envelopes pending delivery per transport | Ciphertext only, survives crash/restart |
scheduled_messages |
Pairwise/group text held until an absolute UTC instant | Plaintext fields exist only inside independently sealed blobs; no ratchet or envelope is created early |
prekeys |
Own signed/PQ/one-time prekey secrets | One-time prekeys deleted on use |
pending |
Inbound envelopes not yet readable (arrived before their session) | Individually sealed; stable row ids; exact-duplicate suppression in the node; 2,048-row / 64 MiB sealed-byte ceiling; TTL-bounded |
rendezvous_service |
Transcript-bound exporter, session binding, provider roles/generations, conflict floors, route-source lease, and bounded retry state | Separately sealed per physical contact device; deleted with the session; explicitly excluded from routine backup |
rendezvous_config |
Complete canonical local provider set and monotonic generation | Separately sealed singleton; runtime network client objects and pending operations are never persisted |
wake_service |
Complete authenticated remote/issued capability sets, generations/conflicts, and bounded trigger retry state | Separately sealed per physical contact device; retired issued capabilities move atomically to the revocation domain; explicitly excluded from routine backup |
wake_revocations |
Identity-free exact gateway revocation retries keyed by a store-scoped digest of provider and opaque capability | 4,096-row installation ceiling; survives restart until generic acknowledgement or expiry; contains no account, device, conversation, message, or social-label identifier; explicitly excluded from routine backup |
media |
Attachment blobs, chunked | Each chunk sealed; keys stored in messages |
ephemeral |
Exact local deadlines, mode, transfer references, active/terminal lifecycle | Sealed separately; terminal tombstones block resurrection after plaintext/media deletion |
local_metadata |
Conversation types, folders, pins, labels, drafts, UI preferences, custom icons | Endpoint-private; only the C2 allowlist can sync to another owned device |
Every database has a random identity. Every record body is individually AEAD-sealed with XChaCha20-Poly1305 and a random 24-byte nonce. Associated data binds the row to that database identity, physical schema version, logical table domain, and final locator. The decoded envelope repeats its logical key and secondary keys; opening recomputes and verifies the keyed locator and every opaque SQLite index before returning the payload.
The physical schema is generic: metadata plus one record table and fixed opaque index slots serve all 35 sensitive logical domains. Pairwise and group history use keyed message-id and conversation indexes, bounded authenticated cursor pages, and indexed exact edit/delete. A copied locked database can still reveal row counts, approximate sizes, insertion order, equality and access patterns within one index domain, and change timing; it does not provide plaintext relationship identifiers as SQLite keys. See ADR-0027 and the opaque-store qualification.
Opening a released plaintext-index schema validates it against the retained release fixture, decrypts and verifies bounded rows, and copies logical records in 256-row transactions into a fresh-id sibling database. A sealed checkpoint supports restart. Counts, references, SQLite integrity, every opaque row, and a complete reopen are checked before the synced sibling atomically replaces the active path. A synced rollback copy remains until that reopen succeeds.
Mailbox relay storage is a separate service boundary
A volunteer mailbox does not write into the endpoint profile database.
mailbox-v2.db has its own random database id, WAL, physical schema, opaque
HMAC indexes, and row-bound XChaCha20-Poly1305 records. Its 32-byte
mailbox-v2.key derives the index and row-sealing keys; a separate
mailbox-v2.transport.key gives the service a stable libp2p address without
using an account identity. Deposits, registrations, leases, exact lease-row
membership, rate buckets, quotas, and expiry survive restart. The schema
validates every sealed logical key and aggregate bound before serving. The
database open forbids a final-component symlink, and the database, WAL, shared
memory sidecar, row key, and transport key are kept owner-only.
This protection is content-blind, not operator-blind. The process necessarily handles tokens, end-to-end ciphertext, expiry, pseudonymous transport peers, random row/lease ids, timing, and volume in live memory. Anyone holding the relay database and service key can recover that relay-visible state, though not message plaintext. Without the key, SQLite still reveals its static schema, approximate row counts/sizes, insertion order, access patterns, and change timing.
The three relay files are not a KKR endpoint backup domain and cannot be reached by the endpoint backup encoder. An operator who promises continued mailbox custody must instead snapshot the stopped relay database and both service keys together under separate encrypted operational backup policy. Restoring only the database or only one key fails closed; starting empty creates a replacement service and does not claim retention of old deposits.
Deletion removes the live logical row. An explicit bounded maintenance call can enable SQLite full secure-delete, incrementally vacuum at most 4,096 pages, and truncate an observed WAL no larger than the caller's bound. The operation reports deferral and always reports that forensic erasure is not guaranteed. These controls reduce live-file remnants but cannot control prior copies, snapshots, flash remapping, recipient devices, or operating-system artifacts.
B9 formatting creates no additional durable state. The messages,
scheduled_messages, group history, and note-to-self rows retain exact source
bytes under their existing seals; formatting markers are not rewritten and no
rendered HTML/attributed text or cache is persisted. KKR10 therefore carries
the same source it already carried and needs no format or migration change.
C3 edits also add no mutable plaintext projection. Canonical originals and edit
events remain separate individually sealed pairwise/group history rows; derived
history hides edit rows and returns the winning text, marker, revision, and
ordered versions. The winner is rebuilt from accepted rows after restart or
restore, including edit-before-original order. Pairwise rows and current group
rows retain authenticated individual origins; legacy group rows retain an
explicit membership-authenticated marker and are never rewritten. KKR10
carries those history rows without live sender chains or recipient origin keys,
so restore requires a fresh group-origin exchange. The node caps locally
authored edits at 64 per target; it retains every accepted inbound edit so
admission order cannot change convergence. See
18: Authenticated Message Editing.
C4 keeps lifecycle state separate from history. Every disappearing/view-once id is keyed by exact conversation, accepted author field, and content id. Pairwise authors and upgraded sender-key group authors are individually authenticated to the recipient; legacy group history remains membership-authenticated. The node sweeps due rows before receive, scheduled activation, attachment work, or queue flush. Expiry/first reveal deletes exact history and queue rows plus every associated media object/chunk, then retains only a sealed terminal tombstone. Duplicate, delayed, reordered, and expiry-before-original delivery cannot rehydrate it. Ordinary export/preview refuses any transfer referenced by a view-once row; terminal consume commits the tombstone before the first output byte. See 19: Disappearing Messages and View-Once Attachments.
C5 polls add no mutable tally or plaintext projection. Creation, vote, and
closure remain separate individually sealed group-history rows. The node
rebuilds the fixed electorate, maximum (revision, event id) vote per member,
winning creator-authenticated closure, and tally on read, so restart and reordered
admission cannot change the result. Local authors are capped at 64 vote
revisions per poll; recipient-authenticated inbound history is retained for
convergence, while released legacy history retains its weaker label. See
20: Group Polls.
C7 live calls add no durable domain. Decoded call control, call/device arbitration, master secrets, derived media keys, replay state, Opus queues, and decoded PCM are transient memory-only state. Call controls do not enter ordinary history, search, scheduled/queued tables, C2 sync, or notification previews; terminal transitions erase secrets and shells clear their protected media buffers. See 23: Live Audio Calls.
ADR-0018 rendezvous keeps optional-service authority outside the serialized Double Ratchet. A verified PQXDH creates a transcript-bound exporter and the store immediately binds it to that ratchet's session id in a separate store-v2 row. Loading attaches it only when the ids match; an orphan or legacy ratchet therefore has no rendezvous authority. The row also contains complete per-provider publish/accept generations, durable route and provider-set conflict floors, a clock floor, source-scoped routes/expiry, and bounded backoff/circuit state. The separately sealed local provider singleton contains only canonical origins, public service keys, ids, and its complete monotonic generation. Runtime clients, in-flight operations, TLS state, and service ciphertext never enter the endpoint database.
B12 stores only the canonical system, light, or dark bytes under the sealed
UI-preference key appearance.theme. Missing or unknown legacy values render as
System without a read-time rewrite. The small shell cache used before unlock is
non-sensitive presentation state; after unlock the sealed F5 value is
authoritative. KKR10 backup and C2 own-device sync carry only that canonical
value, never the pre-unlock cache.
Protected application transients
Desktop and mobile shells sometimes must materialize plaintext because native recorders, decoders, document providers, media players, or user-selected exports operate on files. Those copies are bounded, app-private, backup-excluded where the platform supports it, and named independently of protocol IDs. They are removed on the feature-specific success, discard, denial, failure, lock/background, shutdown, and startup-orphan paths documented in each app README. They never enter SQLite, logs, analytics, notification metadata, or a remote service. This is a bounded endpoint exposure, not a weakening of the sealed durable-store contract.
Private local conversation folders (B10)
FolderRecord stores a locally minted random 16-byte ID, an exact UTF-8 name,
and a persisted manual order. FolderAssignment is keyed by the stable typed
ConversationId for a pairwise peer, group, or note-to-self, so replacing the
row enforces at most one active folder per conversation. All and Unfiled are
virtual views, not definitions. Names retain their exact bytes and follow the
same 256-byte fixed Pattern White Space rule as labels; duplicates are allowed.
Create, rename, complete-set reorder, move/unfile, and delete cascade use local transactions. Deleting a folder removes only its assignments, making those conversations Unfiled without changing messages or conversations. The shared limits are 128 live folders and 8,192 assignments. Missing definitions or conversation targets remain sealed and appear only through render-safe stale diagnostics until explicit cleanup. Folder classification runs before the independent label filter and never changes delivery, notifications, unread truth, search, queue work, or history.
Private contact and conversation labels (B18)
LabelRecord stores a locally minted random 16-byte ID, an exact UTF-8 name,
and a canonical color token. LabelAssignment maps that ID to the stable
ConversationId for a pairwise peer, group, or note-to-self. Identity never
comes from visible text, color, petname, group name, timestamp, or ordering.
Renaming and recoloring preserve the ID, membership, and insertion order;
deletion atomically removes every assignment in the same storage transaction.
Names are retained byte-for-byte without normalization, trimming, case folding,
or rewriting. The front-door limit is 256 UTF-8 bytes. Empty names and names
made only from U+0009–U+000D, U+0020, U+0085, U+200E, U+200F, U+2028, and
U+2029 are rejected. New writes use only neutral, red, orange, yellow,
green, teal, blue, purple, or pink; a legacy unknown token renders as
neutral and is never evaluated as platform code or a resource name.
The shared limits are 128 live definitions, 8,192 live assignments, and 32 labels per conversation. Existing rows above a newer aggregate limit stay readable and removable, while operations that would increase that dimension fail. Assignment and unassignment are idempotent. Unavailable definitions or conversation targets remain available to a render-safe diagnostic and cleanup path but do not participate in active filters. Match-any and match-all filtering is deterministic, local presentation only; it never changes message receipt, delivery, notifications, unread truth, search indexing, queue work, or history.
Private local conversation pins (B11)
Each durable pin is keyed by the exact typed ConversationId: pairwise peer,
group, or note-to-self. Visible names never define identity, and one conversation
has at most one pin. Pin and unpin are idempotent. New pins append to persisted
manual order; when the u32 order space is exhausted, the store transactionally
compacts the complete durable set before appending. The fixed front-door limit is
8,192 pins.
Reorder accepts exactly the complete durable pin set, including unavailable targets, and replaces its order atomically. Missing conversations remain sealed and diagnosable rather than being discarded: the exact typed identity reactivates if that conversation becomes available again, while explicit stale cleanup removes only that unavailable pin. No display-name matching or cross-kind substitution is permitted.
Conversation navigation composes in one deterministic order: folder selection, then label any/all filtering, then a leading pinned block. Eligible pins sort by manual order, recent activity for an otherwise tied legacy order, and stable typed bytes; unpinned rows sort by recent activity and the same stable typed tie-breaker. Pin operations never change messages, folders, labels, search, unread truth, notifications, queues, cryptographic state, or transport work. Message pins remain deferred until stable message-reference semantics are designed separately.
3. Search (planned)
The current Beta does not ship a durable full-text index. A future design may use a separately keyed, bounded local index in which tokenized terms are HMAC'd under a search-domain key before insertion. That design avoids plaintext vocabulary at rest but still leaks repeated-term equality and access patterns inside one database; those trade-offs require implementation, migration, and measurement before search is described as available.
4. Backup & portability
- Encrypted backup file: single-file export (identity + contacts + ordinary history +
local organization/drafts/preferences/icons + note-to-self history +
terminal ephemeral tombstones + signed group authority + linked-device authority,
convergence winners, and session-reset markers), sealed under a key derived from a BIP-39-style
mnemonic via Argon2id.
KKR3added the sealed local metadata domain andKKR4added sealed note-to-self history,KKR5added terminal ephemeral tombstones while excluding every active ephemeral history row, manifest, transfer, and media chunk,KKR6added signed group authority plus bounded consumed admin-request ids, legacyKKR7added the former copied-root linked-device layout, root-freeKKR8introduced the accepted offline-root authority proof, certified endpoints, convergence winners, and eligible user state,KKR9adds durable local block rules, and current root-freeKKR10adds the ADR-0031 discovery capability and generation.KKR1throughKKR7remain explicit decode-only new-identity-reset inputs; no production API creates another copied-root package. Their copied root is opened only in memory, and the destination sibling is built directly from the bounded archive projection under a newly prepared account. No intermediate or published store contains the former root.KKR8andKKR9remain directly restorable predecessors; KKR8 naturally restores no block rows, and both receive a fresh discovery capability. A stable-identityKKR8,KKR9, orKKR10restore additionally requires the separately held.krarecovery-authority file and phrase; it opens that root transiently, advances the recovery epoch, revokes every formerly active device, and mints one fresh physical device. Sessions re-handshake; account root, ratchets, prekeys, sender/receiver chains, link-channel roots, queues, wire ids, reusable device credentials, and delivery-resumption state are deliberately not portable. Queued or sent history restores only as failed local history. Provisional message requests, admission replay tombstones, invitation capabilities, and request notifications are also excluded;KKR10preserves the bounded local account/device block rules needed to avoid silently reopening contact after recovery and the non-zero discovery capability needed to preserve the Connect code. Format and mechanism: ADR-0011, ADR-0026, and ADR-0030. After outer backup decryption, the payload is a bounded stream of logical records; it contains no source database id, opaque index, SQLite page, or wrapped database-row ciphertext. Restore validates the complete logical payload and references, writes a fresh-id sibling database, syncs and atomically installs it, and then fully reopens it. It never exposes a partially restored destination as a valid store. - B18 label backup behavior:
KKR10preserves exact label IDs, names, color tokens, insertion order, assignments, and stale-reference behavior. Labels have no cloud, server, contact, or taxonomy-sharing path; C2 may converge them only between account-authorized owned devices. - B10 folder backup behavior:
KKR10preserves exact folder IDs, names, manual order, single-membership assignments, and stale-reference behavior. Folders have no cloud, server, or contact synchronization; C2 may converge them only between account-authorized owned devices. - B11 pin backup behavior:
KKR10preserves exact typed targets, durable order, and stale/reactivation behavior. Pins have no cloud, server, or contact synchronization; C2 may converge them only between authorized owned devices. - B13 custom-icon backup behavior:
KKR10preserves each canonical sealed PNG under its exact typed contact/group/folder/note-to-self target. Restore reuses the same strict read verification and generated-initials fallback. Icons have no avatar URL, cloud, server, or peer synchronization path; C2 may converge them only between authorized owned devices. The shared caps are 512 KiB per icon, 1,024 records, and 64 MiB total. - Scheduled outbox state is not a backup payload. Like the live encrypted delivery queue, it is device runtime state rather than conversation history; it survives ordinary process/app restarts on that device but is not resurrected by a later identity restore.
- C3 edit backup behavior: originals and accepted edit records ride with ordinary sealed history. Restore recomputes the deterministic winner and prior-version list; it never imports a mutable current-body cache or discards stale losing revisions. Pairwise origin authorization survives restore. Group history preserves its exact origin marker, but live recipient origin capabilities are excluded and must be redistributed after restore.
- C4 ephemeral backup behavior: no live disappearing plaintext, view-once
manifest, or associated media enters
KKR10. Terminal tombstones do, so restore cannot resurrect a removed content id. Active ephemeral content is intentionally non-portable and there is no remote-erasure claim. - C5 poll backup behavior: immutable create, vote, and creator-close rows ride with ordinary sealed group history. Restore derives the same stable IDs, fixed electorate, visible vote heads, closed state, and tally; no mutable counter, new KKR version, or schema migration is involved.
- C6 authority backup behavior:
KKR6introduced the winning canonical signed authority payload, authority event id, owner-transfer chain, and bounded consumed request ids;KKR10carries it forward. The fresh-identity legacy archive reset omits groups rather than accepting their former authority. Sender/receiver chains remain excluded and are refreshed after restore. - C2 linked-device backup behavior: root-free
KKR10carries the public stable account trust anchor, acceptedKDA2proof, certified contact endpoints, convergence winners, eligible ordinary state, and terminal tombstones. It exports no account root, local or contact device private key, prekey, ratchet, group sender/receiver chain, sync/link channel root, rendezvous/wake capability, wake revocation retry, queue, wire id, or resumable delivery state. Restore requires the separately held recovery authority, creates a higher epoch with one fresh device, rotates live relationship state, and rejects old descendants. Legacy copied-rootKKR7cannot be relabelled safe: it requires the visible new-identity reset boundary in ADR-0026. Its archive projection clears contact routes and verification, fails unfinished outbound history, omits groups and all live protocol state, and records every contact still needing a new safety-number comparison. - C7 call backup behavior: no offer/answer/terminal row, call id, device arbitration, secret, media key, Opus packet, or decoded audio enters any KKR version. Restore never resumes or reveals a prior call.
- ADR-0018 rendezvous backup behavior: no hybrid service exporter, provider-set configuration, slot, payload key, registration, accepted route, generation, conflict floor, retry/circuit state, or pending operation enters any KKR version. Restored contacts retain accurately described eligible history but require a fresh authenticated PQXDH exchange before either rendezvous direction exists.
- ADR-0019 wake backup behavior: no remote or issued wake capability, gateway certificate pin, provider identifier, generation/conflict state, pending trigger, retry state, or durable gateway revocation enters any KKR version. Recovery, device replacement, and restored pairwise sessions must establish fresh authenticated capabilities; a backup cannot resume or cancel an old gateway capability.
- Mailbox-service backup behavior: no mailbox database, WAL, registration, deposit, lease, rate bucket, opaque index, relay row key, or mailbox transport identity enters any KKR version. Those files belong to an optional operator role, not the user's portable account. A volunteer operator backs them up only as the separate stopped-service set described above.
- 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 becomes durable or remote state
- Message, draft, contact-name, and media plaintext outside sealed record bodies in the core database or encrypted Komms backups, or intentionally in logs, analytics, crash metadata, or notification metadata. Explicit warned plaintext export and protected lifecycle-bound application transients are the narrow exceptions described above. Keyed equality values and row ciphertext remain inside Komms-owned storage; logs remain structured and content-free by policy.
- Message keys after use; chain keys after advancing (zeroize-on-drop).
- Plaintext contact graphs intentionally uploaded to a remote system. Optional relay queues hold only end-to-end sealed envelopes under rotating tokens with TTLs. Their own durable rows are additionally sealed under a service key, but their operators may still observe live network, token-access, timing, volume, quota, and availability metadata described in the transport threat model.