Configuration and secrets

How a Magik app is configured, where secrets live, and why a missing key fails at boot instead of at 3am.

Status: planned. There is no config layer, no credentials file, no .env loader and no magik credentials command. Rails' encrypted credentials are the prior art being assessed, not a shipped feature. Reviewed 2026-08-26.

The layers, and what wins

Five layers, most specific last. A value from a later layer replaces the same key from an earlier one.

# Layer Where Contains Environments
1 Framework defaults lib/magik/core/ timeouts, pool sizes, log level all
2 App declarations config/app.rb, config/<env>.rb non-secret app settings, seam selection (use :cache, …) all
3 Encrypted credentials config/credentials/<env>.yml.enc secrets: API keys, provider tokens, signing keys all
4 Environment variables the process environment anything, and the only production secret path in a twelve-factor deploy all
5 .env files .env, then .env.local developer convenience development and test only

Precedence, stated once and unambiguously:

framework defaults  <  config/  <  credentials  <  ENV  <  .env (dev/test only)
Why this order
Credentials beat config/ a secret must never be readable in a committed plaintext file, so the encrypted layer overrides the declared one
ENV beats credentials a container platform, a CI runner or a one-off MAGIK_LOG_LEVEL=debug magik server must win without editing an encrypted blob. This is also what makes the same image deployable to several environments
.env beats ENV in dev a developer's local file is the most specific thing on their machine. In production .env is not loaded at all, so the question does not arise

Reading a value is one call, and it never says where the value came from — that is the point of a layer stack:

Magik.config.fetch(:stripe_secret_key)

Encrypted credentials

Rails' model, assessed rather than copied.

config/credentials/production.yml.enc      committed
config/credentials/production.key          NEVER committed, gitignored
config/credentials/development.yml.enc     committed
config/credentials/development.key         NEVER committed
magik credentials edit --env production     # decrypt → $EDITOR → re-encrypt
magik credentials show --env production     # decrypt to stdout
magik credentials diff                      # readable diff of an encrypted file
magik credentials rotate --env production   # new key, re-encrypt, print the new key once

edit decrypts to a temp file with restrictive permissions, opens $EDITOR, re-encrypts on exit, and removes the temp file — including on a crash.

Rails got right Why it matters
Secrets versioned alongside the code that needs them a rollback restores the secrets that release expected; a separate secret store rolls back separately or not at all
One key to deploy the entire production secret payload reduces to a single environment variable
Per-environment files staging cannot accidentally hold production's keys
Rails got wrong / awkward What Magik intends
Key distribution to a team is unsolved — it happens over chat the key is documented as a deployment secret, and the docs point at the platform's own secret store for team distribution. magik credentials rotate exists so a leaked key is a routine event, not an incident
An encrypted blob does not diff, so review is impossible magik credentials diff decrypts both sides and prints a key-level diff — key names and change status, values redacted by default, --show-values for a local review
Losing the key loses the secrets, irrecoverably magik doctor warns when a key is missing, when a .enc file has no key, and when a key is present but unused. The generated app README states plainly that the key has no recovery path
A merge conflict on a .enc file is unresolvable magik credentials edit re-encrypts the whole file, so conflicts are file-level. The documented resolution is: take one side, re-run edit, re-add what the other side changed — using diff to see what that was

.env in development only

.env is developer convenience and never a production mechanism. Production reads real environment variables or credentials.

Rule Detail
Loaded in development and test only. In any other environment the loader does not run, even if the file exists
A .env present in production magik doctor reports it as a finding, and the boot logs a warning naming the file. Silently supporting it is how secrets end up on disk inside a container image
Load order .env first, then .env.local overriding it. Neither is committed
.env.example is committed, is complete — every key the app requires — and carries no real values. Placeholders only
Drift magik doctor compares .env.example against the declared required config and reports keys missing from either side
.env.example     committed, complete, no real values
.env             gitignored — shared team defaults for local work
.env.local       gitignored — one developer's overrides

Declared configuration

Magik is boot-time-enforced everywhere else, and configuration is no exception. An app declares the configuration it requires, and boot fails naming what is missing — rather than a nil surfacing three layers down under load.

App.define :Shop do
  config do
    require :database_url,      :string
    require :stripe_secret_key, :secret, if: -> { billing_enabled? }
    optional :log_level,        :enum, values: %i[debug info warn error], default: :info
    optional :worker_threads,   :integer, default: 4
  end
end
Failure Code fix:
a required key with no value in any layer MAGIK_CONFIG_MISSING_KEY magik credentials edit --env production (or the ENV name, printed)
a value that does not match its declared type or enum MAGIK_CONFIG_INVALID_VALUE the expected shape, with the key name
use :cache, :redis with no REDIS_URL MAGIK_CONFIG_MISSING_KEY the exact key the backend needs
use :cache, :nonexistent MAGIK_CONFIG_UNKNOWN_BACKEND the list of shipped backends

The last two rows are the tie to ../idea/04-swap-points.md: a seam selector is configuration, so a bad selector or a backend missing its own required keys fails at boot with the rest, not at first use — which for a cache backend might be an hour into production traffic.

The intended failure:

MAGIK_CONFIG_MISSING_KEY  config/app.rb:12

  required config :stripe_secret_key was not found in credentials or ENV
  (billing is enabled, provider :stripe)

  fix: magik credentials edit --env production   # add stripe_secret_key
       or set STRIPE_SECRET_KEY in the environment

Boot aborted.

Secrets hygiene, enforced

The spec already refuses field :card_number at the type level. The same principle applied to configuration — and these are mechanisms, not advice:

Rule Mechanism
Never logged values of keys declared :secret, plus a name-pattern list (*_key, *_secret, *_token, *_password, authorization, cookie), are redacted before a log line is written. The redaction list is part of the logging contract (06-observability.md)
Never rendered a :secret value reaching a component raises rather than printing
Never in an error message error cause and details are redacted through the same list before rendering, in all three renderings
Never in a job payload a :secret value serialized into an enqueue argument raises. A job reads the secret from config at run time, where the value is not at rest in a queue table
Never in a trace request traces and OTel attributes go through the same redaction
Redaction is visible redacted output prints [REDACTED:stripe_secret_key], not a blank — so nobody debugs a missing value that was only hidden

Deployment

Concern Intent
How the key reaches a container one environment variable — MAGIK_MASTER_KEY — sourced from the platform's secret store. Never baked into an image, never in the repo
Alternative skip credentials entirely and inject every value as an environment variable. Both paths are first-class; the layer order makes them compose
What is in the image code only. No .env, no .key, no decrypted anything. The image is identical across environments
Rotation magik credentials rotate --env <env> writes a new key, re-encrypts, and prints the key once. Deploy the new key, then the re-encrypted file, then revoke the old — the order matters and the command prints it
Verification magik doctor on boot in a container reports which layer each required key resolved from, values redacted — so a misconfigured deploy is visible in the first log lines

Deployment shape: ../ops/README.md.