Status: Accepted (2026-07-28); implemented for protection declared at
database creation (2026-07-31); per-principal key policy on the peer server
and pgwire (2026-08-03, ADR-0021) —
schema alteration, entity scope, --seal-through, and the operational surface
remain, see the status header in the design. Design in
docs/design/encryption.md. Builds on
ADR-0017 (storage encryption) and extends the
visibility model of ADR-0012 /
ADR-0014.
ADR-0017 protects the medium. It does nothing about a reader inside the
deployment: a peer holds the whole database in memory, a peer server serves many
tenants from one Db, and the transactor sees every fact it commits. The
ViewFilter seam ADR-0012 defined is the policy answer to that, and it has two
limits — it is not implemented on any read path yet, and, more fundamentally, it
is enforced by the process that already has the plaintext. A peer that ignores
policy, or is compromised, discloses everything it holds.
What is wanted instead is a cryptographic floor: some attributes should be unreadable to a reader who was not granted a key, no matter what that reader does — while the peer interface stays exactly as it is, peers keep reading storage themselves and running queries locally, and everything that does not depend on those values keeps working for everyone.
Two engine constraints shape the design. First, the index key is the datom:
a retraction cancels an assertion by sharing its (e, a, v) byte prefix, and
the current-value fold keeps one entry per prefix — so a fact's byte
representation must be stable, on a transactor that will not hold a key.
Second, protected datoms cannot be indexed: ciphertext order is not value
order, so AVET and VAET cannot contain them and no value-ordered access can be
offered over them.
Attributes may declare a protection class; values on those attributes are sealed with that class's key by the writing peer, before the transaction leaves it, and are hydrated only by readers whose keyring resolves the class key.
:db/cas on a transactor with no key. The transactor keeps
doing everything it does today, bytewise.scope/attribute (default) binds
the context to the attribute, so a keyless reader can tell that two entities
share a value. scope/entity also binds the entity: no cross-entity equality,
and the AAD authenticates the subject, so a compromised transactor cannot move
a ciphertext between entities. Entity scope costs a ReserveEntityIds round
trip and an expected_basis_t fence, because sealing must know the entity id
before tempid resolution would have assigned one.:db/protection is
rejected together with :db/index, :db/unique, and :db.type/ref, so
"protected datoms cannot be indexed" is a validation rule rather than a
runtime surprise. Protected datoms live in EAVT and AEVT only, and the planner
treats a bound protected value position as unbound for selectivity, refuses
range access, and never selects AVET.:db/cas old
values. An attribute that has ever been protected may never afterwards gain
:db/index or :db/unique, because a mixed AVET would silently misorder
ranges and silently stop enforcing uniqueness.corium keys audit reports how much plaintext remains and where, and
the alteration must acknowledge its own semantics on the transaction entity.Db. Values stay sealed
in segments, in the live index, and in join keys; ExecOptions carries the
hydrator, so one peer server serves principals with different key sets from
one immutable value. The query cache keys on the key set.KeyPolicy this ADR anticipated is implemented on the peer
server and pgwire (ADR-0021): policy
names key ids on :authz.view/key, and a serving process hands each request
the subset of its own keyring those ids resolve to. Holding a class key is no
longer the same as disclosing it — though that half is enforced by the
process holding the plaintext, and so is authorization rather than
cryptography.:db/cas still works,
because it is a bytewise comparison.AllowFiltered work gains a cheap
first customer — attribute redaction driven by an absent key, enforceable in
the scan with no executor predicate.Value gains a variant matched exhaustively across roughly thirty sites in
six crates, and the thin-client protocol gains a tag (v3). The blast radius is
the cost of making every consumer confront "this value might be unreadable"
rather than discovering it at runtime.corium keys protect --sweep) — but an operator who
protects an attribute and stops there has changed what ordinary readers see
and not what a reader with storage access can recover. That is the honest
cost of forward-only semantics, and it is preferred to the alternatives:
rejecting the alteration outright pushes every deployment into a manual
attribute-renaming migration, and silently rewriting history would break
immutability, the log's authority, and every existing backup and peer.Can you improve this documentation? These fine people already did:
Casey Marshall & ClaudeEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |