Evidence: 3 file:line witnesses as of 2026-08-18. Confidence: partial, some rules witnessed, the rest inherited from the seams. What changes it: the first module built against this doc (
../PLAN.mdforge-1) and its instance breakdown.
One typed config object, defaults that make local work, secrets by reference and never printed. The instances agree on the shape more than on anything else in the matrix, so this doc is short and mostly says why.
Audience: anyone adding a config value or a secret to a module.
What the instances do#
| versable-runner | speedway | walmart-mvp | |
|---|---|---|---|
| shape | one Config class, every var has a default, "the app imports with zero env" (services-api/lib/config/__init__.py:116-122, require_env(name, default=…); runner-service keeps it under overrides/) | one flat object read once from process.env at load, inline comments per var on the prod-vs-dev fallback (app/lib/config.server.ts:1-139) | pydantic BaseSettings, .env + env, lru_cached singleton (backend/app/config.py) |
| secrets | Secret Manager, injected by reference at deploy; 8 secrets | env vars | env vars |
| echo | /health/deep echoes effective runner config | none | none |
| lapse | LANGFUSE_* keys default to empty strings, fine; RUNNER_API_PASSWORD empty silently flips to IAM-locked mode, a behaviour change hidden in an absent value | GCP-specific vars beside vendor-neutral ones with no grouping | none notable |
The rule#
One typed config object per module, read once, with a default for every
value that has a safe one, env as a first-class field, secrets referenced
not embedded, and the effective non-secret config echoed by
/health/deep.
One object, read once#
A module has one config class (pydantic, zod, whatever the language does
well), constructed once at start from environment variables and, locally, a
.env file. Nothing else in the module reads os.environ or process.env.
The reason is the same one that makes the runner/payload seam hold: a raw
read in a payload is a hidden dependency the runner cannot see, mock, or
echo.
Defaults make local true, not prod convenient#
Every value that has a safe local default has one (env: local, in-process
adapters, local storage directory, no tracing keys). Values that must be
supplied in dev and prod (bucket, queue, issuer URL, service URL) have no
default and the module refuses to start without them when env is not
local, naming the missing key. An absent value that silently changes
behaviour is the trap: versable-runner's empty password flipping auth into
IAM-locked mode is a real feature with an invisible switch; the contract
wants that switch to be an explicit auth.verifier: jwks | keys | local
value.
env is a field, and config is validated against it#
env is one of local, dev, staging, prod. The config validates
itself against it at start: no local verifier when env is not local,
no bucket name that belongs to another env, no missing issuer client in
prod. 12-deployment-and-environments.md.
Grouped by concern, named by concern#
Config is grouped the way the canon is: identity.*, queue.*,
storage.*, cache.*, observability.*, usage.*, runtime.*, and
vendors.* for outbound keys. A reader of the config file learns the
module's shape from the group names, and vendor-specific values are visibly
under the adapter that needs them (storage.gcs.bucket, queue.cloudtasks.name).
Secrets#
- Referenced from a secret store and injected at deploy (Secret Manager on
GCP; Render's and Vercel's stores elsewhere), never committed, never in a
Dockerfile, never in a log or a health echo. versable-runner does this
correctly (
deploy.sh:72,--set-secrets … :latest). - Declared in the manifest, so an operator can see what a module needs
without reading its source:
vendorslists each key by env-var name with its purpose and whether the module refuses to start without it (contracts/manifest.md). A key not declared there is a key nobody knows to supply. - Presence is reported, value never:
/health/deepsaysvendors.openai: setorabsent. - Rotated by re-deploying with a new version reference; a module reads a
secret at start and again on a
SIGHUPor a restart, never on every call. - Local
.envfiles are gitignored and carry only local values; a.env.examplewith every key and a dummy value is committed.
The echo#
/health/deep returns the effective config with secrets redacted to
presence, in a stable shape across environments. It is the first thing to
diff when two environments disagree (13-local-dev-and-debugging.md).
Do-nots#
- Do not read an environment variable outside the config object.
- Do not let an absent value silently change behaviour. Make the mode an
explicit value. (versable-runner
RUNNER_API_PASSWORDempty) - Do not default a value that must differ per environment.
- Do not print a secret anywhere: log, echo, error message, transcript.
- Do not commit a
.envwith real values; commit.env.example. - Do not mix vendor-specific and vendor-neutral values in one flat
namespace without grouping. (speedway
config.server.ts)