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:

  1. appsettings.json of 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.
  2. appsettings.{Environment}.json holds per-environment values that are safe to commit: Development points at the Compose services and builds the message tables at startup. Clusters run Production and carry no file of their own.
  3. User secrets (dotnet user-secrets), in Development only, for a host run from the IDE.
  4. 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 ***.

KeyRead byNeeded when
ConnectionStrings:Postgresapi, worker, migrate, messaging-setupalways
ConnectionStrings:Redisapi, workerCaching:Provider is Redis (the chart derives it with the in-cluster Redis)
Ory:WebhookSecretapi (and Kratos, for its calls)always
Stripe:SecretKey, Stripe:WebhookSecretapi, workerbilling is configured
Email:Mailgun:ApiKey, Email:Mailgun:WebhookSigningKeyapi, workerEmail:Provider is Mailgun
Email:Smtp:Passwordapi, workeran SMTP relay that needs one
DataProtection:Certificate, DataProtection:CertificatePasswordapi, workerthe key ring is encrypted with a certificate
Telemetry:OtlpHeadersapi, workerthe OTLP endpoint needs a header (an API key)
Jobs:DashboardPasswordworkerthe job dashboard is reachable
WEB_COOKIE_SECRETwebalways (signs the impersonation cookie)
KRATOS_DSN, KRATOS_SECRETS_COOKIE, KRATOS_SECRETS_CIPHERKratosalways

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

  1. 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 with ValidateRequired.
  2. Add the key to appsettings.json of each host that registers the options (its default, or an empty string for a secret).
  3. List a secret by name in infra/compose/.env.example, with a comment that says what it is and when it is needed.
  4. For a cluster: a non-secret value goes in config (both .NET hosts) or api.config / worker.config of infra/helm/colden/values.yaml when it differs from the default; a secret is added to colden.secretKeys in templates/_helpers.tpl (so a value in the values stops the render) and to the Secret's table in infra/helm/colden/README.md.
  5. 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.