think
16px
820px

Env vs stored settings — audit (2026-08-11)

Question asked: "are there things configurable via env that also have a config
key/setting — remove the env key and rely on config?"

Answer: no env key should be removed. 150 env keys and 15 settings tables were
cross-referenced. Every pair is one of: already seed-then-override, two different axes
wearing similar names, or correctly deployment-only. Two genuine gaps turned up, and both
are missing admin surfaces, not surplus env keys.

The precedent, stated precisely

SMTP is the model, and it is worth being exact about what it did, because "remove the env
key" is not it:

smtp_settings (the admin row) overrides the deployment env for every message. The
env still seeds a fresh install — before anyone can log in there is no row to read,
and an appliance that cannot send its own first invitation cannot be set up.

So the pattern is env seeds, the stored row decides. Deleting the env key breaks first
boot. Nothing below is a candidate for deletion; a few are candidates for gaining a row.


Verdicts

1. AI provider — ALREADY CORRECT, and stricter than SMTP. No action.

AI_CHAT_PROVIDER / AI_CHAT_BASE_URL / AI_CHAT_API_KEY / AI_MODEL +
ai_provider_settings (mig 00146).

Service.resolveProvider applies per-FIELD precedence: a non-empty stored value wins,
an empty one inherits the env, and the admin GET reports each field's origin so the UI can
show which layer is answering. That is finer-grained than SMTP, which swaps the whole row.
Clearing a field in the UI reverts it to the deployment value instead of blanking the
backend.

Cloud: ai_provider_settings is per-tenant (schema-per-tenant), so a tenant's own key wins
over the operator's. Correct already.

2. Embeddings — correctly env-only. Do not add a settings row.

EMBED_PROVIDER / EMBED_MODEL / EMBED_DIM / EMBED_OPENAI_* / EMBED_SIDECAR_URL.

These define the vector space every stored embedding already lives in. Changing the
provider or model at runtime does not reconfigure anything — it silently invalidates the
entire index, because the new vectors are not comparable to the old ones and nothing errors.
That is a deployment-time decision with a re-index attached, not a preference. Leaving it in
env is what keeps it out of reach of a click.

3. Protection / watermarking — not duplication. Two different axes.

key axis
PROTECTION_ENGINE (noop/grpc) wiring — which sidecar, if any, to talk to
protection_settings.default_mode, .classification_modes, .fail_closed policy — what to apply to which classification

An operator without the sidecar deployed cannot be given a policy switch that does nothing;
an operator with it needs per-classification policy the env cannot express. Both layers are
load-bearing and they do not overlap.

Gap found: PROTECTION_NONPDF_EGRESS (audit/block) is policy and has no admin
surface
. The fix is to give it a row, not to remove the key.

4. e-Sign / e-Meterai / Sign-IT — correctly env-only, and deliberately so.

METERAI_PROVIDER, PERURI_*, SIGNIT_*, ESIGN_PROVIDER.

These name a deployment's contract with a provider — corporate id, credentials, and
crucially whether the endpoints are staging or production. validateESignEnvironments
refuses mismatched combinations at boot, because a production meterai against a staging
Sign-IT produces a seal that looks valid and is legally worthless. Moving that behind an
admin UI turns a boot-time refusal into a mis-click.

This turn's update.sh incident is the argument for the current shape, not against it: a
stale env file nearly switched production to sandbox Peruri, and the thing that would have
caught it is exactly this boot validation.

🔴 Cloud finding, flagged not fixed: the sealer is constructed once at boot from
cfg.SignIt.* (wire.go) — there is no per-tenant resolution; only tsaHost varies by
tenant, and that is a Host header, not a credential. So on Cloud every tenant's seal is
issued under the one corporate contract in the deployment env
. Structurally the same shape
as the SMTP relay bug, but the consequence is commercial rather than a leak: whose corporate
identity is on the seal, and whose credit pool pays for it. That is a product decision, not
a defect to quietly patch.

5. Auth — no overlap.

auth_settings holds self-registration, require-TOTP, lockout threshold/minutes and the
password policy. No env key exists for any of those columns. AUTH_MODE,
AUTH_BREAK_GLASS and SESSION_TTL are different concerns (which IdP, whether the
break-glass local login exists, session lifetime) and have no stored twin.

6. Store-only, no env at all — already the end state.

  • rate_limit_settings — every limit is a row; there is no env key.
  • mcp_settings.enabledrequireMCPEnabled reads the store; there is no MCP_ENABLED.
  • dlp_policies, retention_policies, workflow_settings, esign_wa_settings — same.

7. Backup — different axes.

BACKUP_DIR is a path on the host; backup_settings is schedule and retention. An
admin cannot be given the path (it must exist and be writable before the process starts) and
an operator should not have to redeploy to change a retention count.

8. Retention auto-destroy — env-only by design; display-only in the UI.

RETENTION_AUTO_DESTROY / RETENTION_AUTO_DESTROY_DRY_RUN. Admin → Compliance reads and
displays
the posture (off / dry_run / on) without offering to change it.

That is a defensible shape for irreversible automation: visible, not clickable. It is the
one entry on this list where the owner could reasonably decide otherwise — so it is offered
as a decision rather than resolved.


Summary

candidate verdict
AI provider already seed-then-override, per-field. No action
Embeddings correctly env-only (changing it invalidates the index)
Protection engine vs policy different axes, both needed
PROTECTION_NONPDF_EGRESS gap: policy with no admin surface
e-Sign / meterai / Sign-IT correctly env-only (boot-validated legal contract)
e-Sign on Cloud 🔴 process-wide credentials, every tenant seals under one contract
Auth no overlap
Rate limits, MCP, DLP, retention policies, workflow store-only already
Backup dir vs schedule different axes
RETENTION_AUTO_DESTROY env-only, displayed read-only — owner's call

Env keys to remove: none. Two things to decide: whether non-PDF egress policy gets an
admin row, and whether Cloud needs per-tenant e-sign credentials.