# Design decisions

This file records choices that the build brief left open. `FORMAT.md` owns the
wire contract.

## Optimistic rotation instead of row locks

`Encrypted.Rotate` reads a batch without `FOR UPDATE`, decrypts each
old envelope, and updates with both the primary key and old ciphertext in the
predicate. The database writes only when the stored bytes remain unchanged.

This package can use that check because it reads the exact ciphertext bytes
stored in PostgreSQL. A concurrent application write changes those bytes, even
when it writes the same plaintext, because encryption uses a new IV. Holding
row locks during cryptography would increase write contention without adding a
stronger stale-write defence.

A skipped concurrent write may still use an old key if an old application
instance remains active. Operators must deploy the new active key to every
writer and run rotation again until it returns zero before removing an old key.

## Physical storage names define the default context

The default context uses the schema source and the database field source, not
the Elixir field name. The database attacker moves stored bytes between
physical tables and columns, so the binding uses those names.

`key_context: {table, field}` preserves the previous physical names during a
rename. Changing this context for existing data without a matching rotation
makes authentication fail.

## The cleartext header is authenticated

Readers need the version and key ID before they can select a format and key, so
both bytes remain cleartext. AES-GCM authenticates those bytes together with
the packed table and field context. A changed key ID therefore fails even when
two configured IDs accidentally resolve to the same master key.

## Rotation resolves its key ring once

One rotation run resolves all configured master-key sources and derives their
field keys before reading rows. A resolver failure stops the run before any
batch changes data. The active ID and derived keys then remain stable for that
run even if application configuration changes concurrently.

Normal field loads resolve only the key ID named by the envelope. Normal dumps
resolve only the active key. This avoids making an unrelated unavailable old
key block new application writes.

## Package and module names

The Hex package and OTP application use `ecto_encrypted`, which identifies the
Ecto integration. The public type uses `Encrypted`. This keeps the common
schema declaration short while the package remains discoverable by its Ecto
integration.

## Redaction requires an explicit decision

Ecto stores loaded plaintext in the schema struct. `redact: true` removes a
field from the derived `Inspect` output. An omitted field option can therefore
expose plaintext during ordinary inspection.

Schema compilation rejects an encrypted field that has no field-level
redaction decision. Supported Ecto versions do not have one common schema-wide
redaction behavior, so a schema policy does not replace the field option.
`redact: true` is the normal secure choice. `redact: false` remains an explicit
escape hatch for applications that intend to include plaintext in derived
`Inspect` output. Neither choice controls logs that inspect or interpolate
plaintext directly.
