# `PhoenixKit.Integrations.KeyStore`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.14.2/lib/phoenix_kit/integrations/key_store.ex#L1)

Where the integrations encryption secret is kept, as an extension point.

`mix phoenix_kit.integrations.rotate_key` used to print a freshly generated
secret exactly once and save it nowhere. An operator who lost that line lost
every stored integration credential with it — the ciphertext stays in the
database and nothing can read it again. This behaviour exists so the secret
has somewhere to land.

## The default is deliberately boring

`PhoenixKit.Integrations.KeyStore.File` writes one file, mode `0600`, outside
the repository. No external service, no account, no credentials of its own —
it works for any PhoenixKit user on any host, which a cloud secrets manager
does not. Hosts that want Vault, AWS Secrets Manager or anything else
implement this behaviour and configure their module instead; nothing here
assumes the default.

## Configuring

    config :phoenix_kit, integrations_key_store: PhoenixKit.Integrations.KeyStore.File

    # or, with options
    config :phoenix_kit,
      integrations_key_store: {PhoenixKit.Integrations.KeyStore.File, path: "/etc/phoenix_kit/app.key"}

Unconfigured is a supported state, not a broken one: rotation keeps its old
print-once behaviour and says plainly that the secret was saved nowhere.

## Reading

`PhoenixKit.Integrations.Encryption` consults the store when no explicit
`:integrations_encryption_key` is configured, so a rotation followed by a
restart needs no config edit. The read is memoised in `:persistent_term` —
encryption runs per credential and must not hit the filesystem each time.
Rotation invalidates that cache, and so can `invalidate_cache/0`.

## Secrets never travel in error terms

Every callback returns the *path* or a reason, never the secret. A secret in
an error tuple ends up in a log, a crash report or a support ticket, and a
secret that reaches a log is compromised in the only sense that matters.

# `configured`

```elixir
@type configured() :: {module(), keyword()}
```

A store implementation plus the options it was configured with.

# `describe`

```elixir
@callback describe(opts :: keyword()) :: String.t()
```

Human-readable location, for operator-facing messages. Never the secret.

# `preflight`

```elixir
@callback preflight(opts :: keyword()) :: :ok | {:error, term()}
```

Checks the store can be written to, without writing the real secret.

Called before a rotation re-encrypts anything. Rotation is the dangerous
moment: once rows are re-encrypted, a store that then refuses the write
leaves the operator holding a database no key opens. Finding out first turns
that into an abort that changed nothing.

# `read`

```elixir
@callback read(opts :: keyword()) ::
  {:ok, String.t()} | :not_configured | {:error, term()}
```

Reads the stored secret.

`:not_configured` means the store has nothing yet (a first run), which is
different from `{:error, reason}` — an existing secret that could not be
read. Callers must not collapse the two: "no key yet" invites writing one,
"could not read" must never lead to overwriting one.

# `write`

```elixir
@callback write(secret :: String.t(), opts :: keyword()) :: :ok | {:error, term()}
```

Persists `secret`, replacing whatever was there.

Implementations must not log the secret, and must not leave a partially
written file behind on failure.

# `cached_read`

```elixir
@spec cached_read() :: {:ok, String.t()} | :not_configured | {:error, term()}
```

Memoised `read/0`, for the encryption hot path.

Encryption runs once per credential field; an unmemoised read would stat and
open a file on every one. The cache is invalidated by `invalidate_cache/0`,
which rotation calls after storing a new secret.

# `configured`

```elixir
@spec configured() :: configured() | nil
```

The configured store as `{module, opts}`, or `nil`.

Accepts a bare module or a `{module, opts}` tuple.

# `configured?`

```elixir
@spec configured?() :: boolean()
```

Whether a store is configured at all.

# `describe`

```elixir
@spec describe() :: String.t() | nil
```

Where the configured store keeps the secret, for messages. Never the secret.

# `describe_error`

```elixir
@spec describe_error(term()) :: String.t()
```

A short description of a store error, safe to put in a log or a message.

Deliberately NOT `inspect/1` of the whole term. The reasons produced here are
known to be safe, but a host-supplied store returns whatever it likes from
`read/1` and `write/2` — including, plausibly, an error that quotes the value
it failed to store. Only the recognised shapes are formatted; anything else is
reduced to its outermost tag, so an unknown term cannot carry a secret into a
log by accident.

# `invalidate_cache`

```elixir
@spec invalidate_cache() :: :ok
```

Drops the memoised secret. Safe to call when nothing is cached.

# `invoke_store`

```elixir
@spec invoke_store(module(), atom(), [term()]) :: term()
```

Calls a store callback without letting a secret escape into an exception.

Public because `PhoenixKit.Integrations.KeyStore.Chain` calls member stores
and must not bypass this: `apply/3` on a mistyped or unloaded module raises,
and Erlang formats such reports with their arguments — which for `write/2` is
the secret.

# `preflight`

```elixir
@spec preflight() :: :ok | :not_configured | {:error, term()}
```

Runs the configured store's pre-flight check.

`:not_configured` when there is no store — not an error; the caller decides
whether that is acceptable.

# `read`

```elixir
@spec read() :: {:ok, String.t()} | :not_configured | {:error, term()}
```

Reads the secret from the configured store.

Returns `:not_configured` both when no store is configured and when the
configured store holds nothing yet — from a caller's point of view those are
the same situation: there is no secret to use.

# `write_verified`

```elixir
@spec write_verified(String.t()) :: :ok | :not_configured | {:error, term()}
```

Writes the secret, then reads it back and compares before reporting success.

A write that reports `:ok` and did not land is the failure this whole module
exists to prevent, and it is not hypothetical: a key file was lost this way
on 2026-08-18 — the file looked intact and the value inside was empty. So the
write is not trusted on its own word; it is verified by reading.

`{:error, {:verification_failed, _}}` means the data may be re-encrypted
while the secret is NOT safely stored. Callers must treat that as loud.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
