Configuration
How the kit's hosts read configuration: the layers, the environment-variable form, the secrets, adding a key, how values reach Compose and a cluster, and changing a value or a secret.
The api and the worker read configuration the way ASP.NET Core does out of the box and from nothing else; the web reads a handful of environment variables at request time. Every package binds a typed options class, validates it at startup, and never prints a secret. The conventions of the options classes are in packages/core/Colden.Core/Configuration/README.md; this guide is the operator's and the developer's view.
The layers
From the lowest to the highest priority:
appsettings.jsonof each host (apps/api/Colden.Api,apps/worker/Colden.Worker) declares every key the host reads, with its default, or an empty string for a secret or a value each environment must give.appsettings.{Environment}.jsonholds per-environment values that are safe to commit:Developmentpoints at the Compose services and builds the message tables at startup. Clusters runProductionand carry no file of their own.- User secrets (
dotnet user-secrets), in Development only, for a host run from the IDE. - Environment variables, which win over every file.
Nothing else is read: no remote configuration service, no file mounted at runtime. A change takes a restart.
Environment variables
A key Section:Key is the variable Section__Key; a nested key doubles each separator (Messaging:Retry:Attempts is Messaging__Retry__Attempts), and a list takes indexes (Section__List__0). Values are strings the binder converts: true, 30, an enum's name (SchemaPerTenant).
Secrets
A secret is a property marked [Secret]. It is empty in every appsettings.json (a test of each host fails otherwise), listed by name in infra/compose/.env.example (another test checks it), and set only as an environment variable: from infra/compose/.env (git-ignored) in Compose, from user secrets for a host run from the IDE, and from the Kubernetes Secret in a cluster. The diagnostics show a secret that has a value as ***.
| Key | Read by | Needed when |
|---|---|---|
ConnectionStrings:Postgres | api, worker, migrate, messaging-setup | always |
ConnectionStrings:Redis | api, worker | Caching:Provider is Redis (the chart derives it with the in-cluster Redis) |
Ory:WebhookSecret | api (and Kratos, for its calls) | always |
Stripe:SecretKey, Stripe:WebhookSecret | api, worker | billing is configured |
Email:Mailgun:ApiKey, Email:Mailgun:WebhookSigningKey | api, worker | Email:Provider is Mailgun |
Email:Smtp:Password | api, worker | an SMTP relay that needs one |
DataProtection:Certificate, DataProtection:CertificatePassword | api, worker | the key ring is encrypted with a certificate |
Telemetry:OtlpHeaders | api, worker | the OTLP endpoint needs a header (an API key) |
Jobs:DashboardPassword | worker | the job dashboard is reachable |
WEB_COOKIE_SECRET | web | always (signs the impersonation cookie) |
KRATOS_DSN, KRATOS_SECRETS_COOKIE, KRATOS_SECRETS_CIPHER | Kratos | always |
Validation
A host with a missing or invalid value stops before it serves: it prints one message that names every failing key (never a value) and exits with code 64, which a Kubernetes pod shows as a crash loop with that message in its log. /health/ready carries the same check as configuration. In Development a valid configuration is printed once at startup with the secrets masked, which answers "which value did the host read?".
Adding a key
- Add the property to the package's options class, with its default and its data annotations (
[Required],[Range],[RegularExpression]); mark a secret[Secret]. A key required only in some configurations is declared withValidateRequired. - Add the key to
appsettings.jsonof each host that registers the options (its default, or an empty string for a secret). - List a secret by name in
infra/compose/.env.example, with a comment that says what it is and when it is needed. - For a cluster: a non-secret value goes in
config(both .NET hosts) orapi.config/worker.configofinfra/helm/colden/values.yamlwhen it differs from the default; a secret is added tocolden.secretKeysintemplates/_helpers.tpl(so a value in the values stops the render) and to the Secret's table ininfra/helm/colden/README.md. - Add the key to the configuration table of
docs/architecture/colden-architecture.html, and to the package's README.
Run the hosts' tests: AppSettingsCoverage fails when appsettings.json misses a key or gives a secret a value, and EnvExampleCoverage when .env.example misses a secret.
How values reach Compose and a cluster
Compose
Compose reads infra/compose/.env for its own settings and passes it to every kit container (env_file), where each Section__Key overrides appsettings.json. The services add the values that follow from the stack (the connection strings to postgres and redis, Mailpit as the SMTP host, the identity server's addresses). The containers run ASPNETCORE_ENVIRONMENT from .env, Development by default.
A cluster
The chart turns the values into a ConfigMap per host (the api, the worker, the web and Kratos): the derived settings (the environment, the identity server's and the web's addresses, the in-cluster Redis), then config for both .NET hosts, then the host's own config. The pods read the ConfigMap with envFrom, then the Secret named by existingSecret (default colden-secrets), so a secret wins over a setting of the same name. The deploy pipeline creates that Secret before helm upgrade (tools/bootstrap-namespace.sh); the chart never holds a secret value, and a secret's name in the values stops the render. The migration hooks read only the Secret (and the few settings they need inline), because they run before the release's ConfigMaps change.
Changing a value or a secret
-
A non-secret value: change it in the environment's values file (
infra/helm/environments/<owner>/<environment>.yaml) and deploy. Each Deployment carries the checksum of its ConfigMap, so the pods that read the value roll, and only those. -
A secret: change the variable where the pipeline reads it, rerun
tools/bootstrap-namespace.sh <namespace>(the pipeline does it on every deployment), then restart the pods, since nothing reloads a secret at runtime:kubectl -n <namespace> rollout restart deploy -l app.kubernetes.io/instance=<release>
The web
The web reads its variables at request time, so one image serves every environment: APP_BASE_URL, COLDEN_API_URL, KRATOS_PUBLIC_URL, WEB_COOKIE_SECRET, MAINTENANCE_MODE, MAINTENANCE_UNTIL, STATUS_PAGE_URL and SUPPORT_URL (apps/web/.env.example describes each). Compose sets them on the web service; the chart derives the addresses and takes the rest from web.config. The browser never sees a variable: client components get what they need from the server.