Files
member-console/docs/environment-reference.md
T
cgalo5758 2d71aa8db0 Describe the boot that serves before its dependencies
Seventh section of #141, the docs. docs/production-deployment.md's first
boot section lists the steps in the order start runs them, says what
serves from the moment the listener opens, and describes the two loops
that run beside it: the identity provider's discovery, with sign-in at
503 until a usable document is published and roles not re-derived
before a document with the configured issuer is read, and Temporal,
retried for the life of the process with its log turning to Error after
temporal-connect-timeout. The service list says that Stripe and
Discourse need Temporal. README.md and docs/plan-management.md lose
their description of a bounded retry before the server comes up.

docs/environment-reference.md rewrites the temporal-connect-timeout and
temporal-host rows; docs/stripe.md and docs/discourse-setup.md say why
temporal-host is required; docs/building-an-integration.md documents
NeedsTemporal. docs/fedwiki-setup.md says the console answers the
certificate ask as soon as it serves. docs/identity-provider-setup.md
and internal/auth/README.md replace "refuses to start" with what the
console does, and docs/temporal-authorization-setup.md explains the
token URL. Comments in the FedWiki and Stripe integrations and the
FedWiki workflows README that still described the old boot are
corrected. Every quoted log line is the one the code writes.

No code changes. make test passes with the test stack up, and go test
-race -p 1 over cmd, auth, server, retryloop, workflows and the three
integrations after reset-test-db.sh finds no data race.
2026-10-23 21:07:48 -04:00

201 lines
18 KiB
Markdown

---
title: "Environment Variable Reference"
audience: [admin, developer]
summary: "Every configuration key member-console reads: purpose, default, required/optional, and its derived MC_ environment variable override."
---
# Environment Variable Reference
member-console reads configuration through [Viper](https://github.com/spf13/viper),
layered in this precedence order (highest wins): command-line flag, `MC_*`
environment variable, `mc-config.yaml` (or the file passed to `--config`),
built-in default. `internal/config/config_reference_test.go` asserts every
configuration key the source code reads is documented on this page.
**Derivation rule:** every key below is written in `mc-config.yaml` as its
kebab-case name (e.g. `db-dsn`). Its environment variable override is
`MC_` followed by that name upper-cased with every `-` replaced by `_` (e.g.
`MC_DB_DSN`). This is a single mechanical rule (`cmd/root.go`:
`viper.SetEnvPrefix("MC")` + `viper.SetEnvKeyReplacer(strings.NewReplacer("-",
"_"))`) applied uniformly; the table below states each key once and omits the
env column's value where it is a pure derivation, but always names the env
form so it can be found by search.
**Secret keys and `-file` variants.** A key marked *secret* also accepts a
`<key>-file` variant (env `MC_<KEY>_FILE`) whose value is a filesystem path;
at boot, member-console reads the file's contents into the key. Setting both
the direct value and the `-file` variant for the same key is a fatal
misconfiguration (the process exits before starting any service). This is
how Docker/Podman secrets and Kubernetes-mounted secrets are wired in without
putting secret material in the environment or the config file; see
[production-deployment.md](production-deployment.md).
**Type.** Every key's value is one of a closed set: `string`, `url`
(absolute, `http` or `https`, with a host), `duration` (a Go duration
string, e.g. `1h30m`; never negative), `int`, `bool` (`true`/`false`), `list`
(comma-separated), or an enum's members joined by `|`. `duration, greater
than zero` marks a duration key whose own consumer replaces a saved zero
with its default silently, so zero is refused rather than accepted and
ignored (design D6, `typed-config-keys`). Every declared integration key's
Type column is checked against its declaration by
`internal/config/config_reference_test.go`; core keys are filled by hand
from `internal/config/validate.go`'s (`ValidateStart`) checks.
## Core
| Key | Type | Env override | Purpose | Default | Required |
|---|---|---|---|---|---|
| `port` | string | `MC_PORT` | Port the HTTP server listens on. | `8080` | Optional |
| `base-url` | url | `MC_BASE_URL` | Public URL of this console, e.g. `https://console.example.com`; `http` or `https`. Its origin is the console's sole cross-origin trusted origin, and its scheme decides the session cookie's `Secure` flag, the CSP `upgrade-insecure-requests` directive, and the `Strict-Transport-Security` header (all on for `https`, off for `http`); see [production-deployment.md](production-deployment.md). | none | Required |
| `env` | string | `MC_ENV` | Environment label. `development` selects text log formatting; any other value selects JSON. Nothing security-relevant hangs off it. | `development` | Optional |
| `deployment-name` | string | `MC_DEPLOYMENT_NAME` | Name this deployment presents for itself on the member and operator mastheads, page titles, and OpenGraph tags. | `Member Console` | Optional (rejected if set to blank/whitespace-only) |
| `support-url` | url | `MC_SUPPORT_URL` | URL shown in error messages for users to get support. | none | Optional |
## Database and session store
| Key | Type | Env override | Purpose | Default | Required |
|---|---|---|---|---|---|
| `db-dsn` | url (postgres) | `MC_DB_DSN` | PostgreSQL connection string of the runtime user, the user the console serves from: not a superuser, owns nothing, and holds no role but `console_runtime`. E.g. `postgres://user:pass@host:5432/dbname?sslmode=disable`. Use this or `db-dsn-file`, never both. Treated as a secret: never logged; the boot log names host, port, database and user only. | none | One of this or `db-dsn-file` |
| `db-dsn-file` | path | `MC_DB_DSN_FILE` | File holding the runtime user's PostgreSQL DSN, read at startup. | none | One of this or `db-dsn` |
| `db-migrate-dsn` | url (postgres) | `MC_DB_MIGRATE_DSN` | PostgreSQL connection string of the migration user, the user that owns the schemas. `start` runs the migrations and grants `console_runtime` to the runtime user on this connection, then closes it; with the key unset `start` runs no migrations and refuses a schema that is behind the code. `member-console migrate up\|down\|status` use this key and refuse to run without it. Use this or `db-migrate-dsn-file`, never both. Treated as a secret: never logged; the boot log names host, port, database and user only. | none | Optional for `start`; required by `migrate` |
| `db-migrate-dsn-file` | path | `MC_DB_MIGRATE_DSN_FILE` | File holding the migration user's PostgreSQL DSN, read at startup. | none | Optional |
| `valkey-addr` | string | `MC_VALKEY_ADDR` | Valkey/Redis address (`host:port`) for server-side session storage. | `localhost:6379` | Required (satisfied by the default unless explicitly cleared) |
| `valkey-username` | string | `MC_VALKEY_USERNAME` | ACL username for the session store, when it uses one. | none | Optional |
| `valkey-password` | string | `MC_VALKEY_PASSWORD` | Password for the session store. Use this or `valkey-password-file`, never both. | none | **Required** (the store must not be reachable unauthenticated) |
| `valkey-password-file` | path | `MC_VALKEY_PASSWORD_FILE` | File holding the session-store password, read at startup. | none | One of this or `valkey-password` |
| `valkey-tls` | bool | `MC_VALKEY_TLS` | Connect to the session store over TLS. | `false` | Optional |
| `valkey-tls-skip-verify` | bool | `MC_VALKEY_TLS_SKIP_VERIFY` | Do not verify the session store's TLS certificate. Self-signed stores only; logs a warning at every boot. | `false` | Optional |
## Identity (OIDC)
Cross-origin request protection has no configuration key: it is the server's
origin check, trusting `base-url`'s origin (`internal/middleware/csrf.go`). A
configuration that still carries `csrf-secret` boots; the key is not read.
| Key | Type | Env override | Purpose | Default | Required |
|---|---|---|---|---|---|
| `oidc-idp-issuer-url` | url | `MC_OIDC_IDP_ISSUER_URL` | OIDC issuer URL, e.g. `https://idp.example.com/realms/main`. | none | Required |
| `oidc-idp-account-url` | url | `MC_OIDC_IDP_ACCOUNT_URL` | URL of the identity provider's self-service account console, opened by the shell's Identity and Access control. OpenID discovery defines no such URL, so providers other than Keycloak set it here. | `{oidc-idp-issuer-url}/account` (Keycloak's shape) | Optional |
| `oidc-sp-client-id` | string | `MC_OIDC_SP_CLIENT_ID` | OIDC client ID registered for this console. | none | Required |
| `oidc-sp-client-secret` (secret) | string | `MC_OIDC_SP_CLIENT_SECRET` | OIDC client secret. | none | Not enforced by boot validation; set it (or `-file`) for a confidential OIDC client |
| `oidc-sp-client-secret-file` | string | `MC_OIDC_SP_CLIENT_SECRET_FILE` | File path holding `oidc-sp-client-secret`. | none | Optional |
See [identity-provider-setup.md](identity-provider-setup.md) for configuring the IdP side of this exchange.
## Temporal
| Key | Type | Env override | Purpose | Default | Required |
|---|---|---|---|---|---|
| `temporal-host` | string | `MC_TEMPORAL_HOST` | Temporal server `host:port`, e.g. `localhost:7233`. Leaving it unset disables Temporal, the worker, integration workflows, and every scheduled sweep below. A configured Stripe or Discourse needs it: both receive events that only Temporal processes, and `start` refuses the configuration without it. | none | Required when Stripe or Discourse is configured; otherwise optional (functionally required for provisioning and scheduled jobs) |
| `temporal-namespace` | string | `MC_TEMPORAL_NAMESPACE` | Temporal namespace. | `default` | Required once `temporal-host` is set |
| `temporal-oauth-token-url` | url | `MC_TEMPORAL_OAUTH_TOKEN_URL` | OAuth2 token endpoint for Temporal client authentication. | none | Optional; if any `temporal-oauth-*` key below is set, all three must be |
| `temporal-oauth-client-id` | string | `MC_TEMPORAL_OAUTH_CLIENT_ID` | OAuth2 client ID for Temporal authentication. | none | Optional; see above |
| `temporal-oauth-client-secret` (secret) | string | `MC_TEMPORAL_OAUTH_CLIENT_SECRET` | OAuth2 client secret for Temporal authentication. | none | Optional; see above |
| `temporal-oauth-client-secret-file` | string | `MC_TEMPORAL_OAUTH_CLIENT_SECRET_FILE` | File path holding `temporal-oauth-client-secret`. | none | Optional |
| `temporal-oauth-scopes` | list | `MC_TEMPORAL_OAUTH_SCOPES` | OAuth2 scopes for Temporal authentication (comma-separated). | none | Optional |
| `temporal-connect-timeout` | duration | `MC_TEMPORAL_CONNECT_TIMEOUT` | Time after startup from which failed Temporal connection attempts log at Error. The console keeps retrying for the life of the process, so the value sets when the log turns to Error and never stops the retries. | `90s` | Optional |
See [temporal-authorization-setup.md](temporal-authorization-setup.md) for configuring the IdP side of Temporal's own JWT authorization.
## Custom domains (external domain claims)
Members with the capability can point their own domain at a FedWiki site or
other resource; these keys tune that subsystem. All are optional; leaving
`domains-connect-target` unset disables external domain claims
deployment-wide.
| Key | Type | Env override | Purpose | Default | Required |
|---|---|---|---|---|---|
| `domains-connect-target` | string | `MC_DOMAINS_CONNECT_TARGET` | DNS target members point external custom domains at (CNAME host or A-record IP). Empty disables external domain claims deployment-wide. | none | Optional |
| `domains-ask-fallback-url` | url | `MC_DOMAINS_ASK_FALLBACK_URL` | Legacy on-demand-TLS answerer consulted for names unknown to the domains registry. Empty refuses unknown names. | none | Optional |
| `domains-claim-window` | duration | `MC_DOMAINS_CLAIM_WINDOW` | How long a member has to publish the TXT challenge before a pending claim expires. | `24h` | Optional |
| `domains-pending-cap` | int | `MC_DOMAINS_PENDING_CAP` | Concurrent pending external domain verifications allowed per workspace. | `5` | Optional |
| `domains-abandon-budget` | int | `MC_DOMAINS_ABANDON_BUDGET` | Verifications a workspace may start and abandon under one domain scope, within the abandonment window, before further claims are refused. `0` disables the ledger. | `3` | Optional |
| `domains-abandon-window` | duration | `MC_DOMAINS_ABANDON_WINDOW` | Rolling window abandoned verifications are counted over. | `168h` | Optional |
| `domains-scope-labels` | int | `MC_DOMAINS_SCOPE_LABELS` | Trailing DNS labels forming the scope abandonments are counted under, so sibling names share one scope. | `2` | Optional |
| `domains-initiation-budget` | int | `MC_DOMAINS_INITIATION_BUDGET` | New external domain claims allowed per workspace per rolling 24h. `0` disables the cap. | `10` | Optional |
| `domains-expiry-sweep-interval` | duration | `MC_DOMAINS_EXPIRY_SWEEP_INTERVAL` | Interval between the Temporal-backed domain-claim expiry sweep (backstop for claims whose verification workflow is gone). | `15m` | Optional |
## Billing and maintenance sweeps
| Key | Type | Env override | Purpose | Default | Required |
|---|---|---|---|---|---|
| `billing-sweep-interval` | duration | `MC_BILLING_SWEEP_INTERVAL` | Interval between billing scheduled-change sweeps (backstop firing path for due subscription changes). | `1h` | Optional |
| `webhook-partition-ensure-interval` | duration | `MC_WEBHOOK_PARTITION_ENSURE_INTERVAL` | Interval between `core.webhook_events` partition-ensure runs, keeping monthly partitions provisioned ahead of need. | `24h` | Optional |
## Stripe (optional; only for paid products)
| Key | Type | Env override | Purpose | Default | Required |
|---|---|---|---|---|---|
| `stripe-api-key` (secret) | string | `MC_STRIPE_API_KEY` | Stripe secret API key. | none | Required together with `stripe-webhook-secret` to enable billing; both empty leaves it disabled |
| `stripe-api-key-file` | string | `MC_STRIPE_API_KEY_FILE` | File path holding `stripe-api-key`. | none | Optional |
| `stripe-webhook-secret` (secret) | string | `MC_STRIPE_WEBHOOK_SECRET` | Stripe webhook endpoint signing secret. | none | Required together with `stripe-api-key` |
| `stripe-webhook-secret-file` | string | `MC_STRIPE_WEBHOOK_SECRET_FILE` | File path holding `stripe-webhook-secret`. | none | Optional |
See [stripe.md](stripe.md) for the webhook endpoint, subscribed events, and catalog sync.
## FedWiki (optional; the FedWiki farm connection)
| Key | Type | Env override | Purpose | Default | Required |
|---|---|---|---|---|---|
| `fedwiki-farm-api-url` | url | `MC_FEDWIKI_FARM_API_URL` | URL of the FedWiki farm's FarmManager API endpoint, e.g. `https://admin.wiki.example.com`. | none | Required together with `fedwiki-admin-token`; both empty leaves the farm connection disabled |
| `fedwiki-admin-token` (secret) | string | `MC_FEDWIKI_ADMIN_TOKEN` | FarmManager API admin access token. | none | Required together with `fedwiki-farm-api-url` |
| `fedwiki-admin-token-file` | string | `MC_FEDWIKI_ADMIN_TOKEN_FILE` | File path holding `fedwiki-admin-token`. | none | Optional |
| `fedwiki-allowed-domains` | list | `MC_FEDWIKI_ALLOWED_DOMAINS` | Domains where users can create FedWiki sites (comma-separated), e.g. `wiki.example.com,wiki2.example.com`. | none | Optional |
| `fedwiki-site-scheme` | http\|https | `MC_FEDWIKI_SITE_SCHEME` | URL scheme for generated FedWiki site links: `http` or `https`. | `https` | Optional |
| `fedwiki-sync-enabled` | bool | `MC_FEDWIKI_SYNC_ENABLED` | Enable periodic sync of sites from the FedWiki farm. | `false` | Optional |
| `fedwiki-sync-interval` | duration, greater than zero | `MC_FEDWIKI_SYNC_INTERVAL` | Interval between FedWiki site syncs. Inert while sync is disabled. | `1h` | Optional |
| `fedwiki-sync-trigger-immediately` | bool | `MC_FEDWIKI_SYNC_TRIGGER_IMMEDIATELY` | Run a sync immediately once the console connects to Temporal, in addition to the interval. | `true` | Optional |
| `fedwiki-swap-cooldown` | duration | `MC_FEDWIKI_SWAP_COOLDOWN` | Cooldown window between FedWiki active-site rotations (swap or archive-then-restore). `0` disables it. | `720h` (30 days) | Optional |
See [fedwiki-setup.md](fedwiki-setup.md) for the FarmManager security stack and local dev setup.
## Discourse (optional)
| Key | Type | Env override | Purpose | Default | Required |
|---|---|---|---|---|---|
| `discourse-base-url` | url | `MC_DISCOURSE_BASE_URL` | Base URL of the Discourse instance, e.g. `https://forum.example.com`. | none | Required together with `discourse-api-key`; both empty leaves the integration dormant |
| `discourse-api-key` (secret) | string | `MC_DISCOURSE_API_KEY` | Discourse admin API key (global key, used with `discourse-api-username`). | none | Required together with `discourse-base-url` |
| `discourse-api-key-file` | string | `MC_DISCOURSE_API_KEY_FILE` | File path holding `discourse-api-key`. | none | Optional |
| `discourse-api-username` | string | `MC_DISCOURSE_API_USERNAME` | `Api-Username` header value for Discourse admin API calls. | `system` | Optional |
| `discourse-linkage-mode` | email\|oidc\|discourseconnect | `MC_DISCOURSE_LINKAGE_MODE` | How persons are matched to Discourse users: `email`, `oidc`, or `discourseconnect`. | `oidc` | Optional |
| `discourse-auto-create-users` | false\|true | `MC_DISCOURSE_AUTO_CREATE_USERS` | Create Discourse accounts for entitled persons with no forum user (`true`/`false`). The default only finds and links existing users. | `false` | Optional |
| `discourse-webhook-secret` (secret) | string | `MC_DISCOURSE_WEBHOOK_SECRET` | Shared secret verifying Discourse webhook signatures. Leave empty to disable the webhook endpoint. | none | Optional |
| `discourse-webhook-secret-file` | string | `MC_DISCOURSE_WEBHOOK_SECRET_FILE` | File path holding `discourse-webhook-secret`. | none | Optional |
| `discourse-sync-interval` | duration, greater than zero | `MC_DISCOURSE_SYNC_INTERVAL` | Interval between Discourse group-sync sweeps. | `15m` | Optional |
| `discourse-sync-trigger-immediately` | bool | `MC_DISCOURSE_SYNC_TRIGGER_IMMEDIATELY` | Run a group-sync sweep immediately once the console connects to Temporal. | `false` | Optional |
See [discourse-setup.md](discourse-setup.md) for forum-side prerequisites and linkage-mode selection.
## Integration settings overlay
Every key above that is part of an installed integration's declared spec
(FedWiki, Stripe, Discourse) can also be reviewed and overridden after boot
from the operator Integration Settings UI, without touching `mc-config.yaml`
or the environment. An override an operator saves there is stored in
`core.integration_config_overrides` and layered over the environment's value
at the *next* boot (`internal/config.ApplyOverlay`); it is not read live, so
a saved override takes effect on restart. The overlay is read once, during
single-threaded boot, so an override that no longer parses has no running
value to fall back on and boot fails, naming the key. PostgreSQL's split,
fatal at start and ignored with a log entry at reload, is the rule to copy
if the overlay ever gains a reload path. Secret-marked keys are never
overridable from this UI; only their presence is shown, never their value
(`internal/config/validate.go:180`, `internal/config.RequiredKeysUnresolved`).
This page does not restate each integration's key list a second time for
the UI; the keys are the same ones documented above.
## Direct environment reads outside Viper
| Variable | Type | Purpose | Default |
|---|---|---|---|
| `LOG_LEVEL` | debug\|info\|warn\|error | Log verbosity, read directly via `os.Getenv` in `internal/logging/logging.go` (not a Viper/`MC_*` key). Accepts any value `slog.Level.UnmarshalText` parses (case-insensitive). An unparseable value panics at boot. | `info` |
## Test-only environment variables
`TEST_DATABASE_URL` is read directly (`os.Getenv`) by the Go test suite's
DB-backed tests; it is not a member-console application configuration key and
has no `MC_*` form. It, `E2E_DATABASE_URL`, and `POSTGRES_ADMIN_DSN` are part
of the test-stack contract documented in [`test/AGENTS.md`](../test/AGENTS.md)
and [`testing.md`](testing.md), not this reference.