Skip to content

feat(config)!: configure Fanout from the environment only - #305

Merged
vishr merged 6 commits into
mainfrom
config/consumer-env-dotenv
Oct 14, 2026
Merged

vishr merged 6 commits into
mainfrom
config/consumer-env-dotenv

Conversation

@vishr

@vishr vishr commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

What changed

Fanout is now configured from the environment only. Built-in defaults live in internal/config/config.go, and the process environment overrides them. The YAML loader, --config, fanout.example.yaml, fanout.docker.yaml, and koanf are removed. All settings are flat, so a second configuration source added precedence rules without adding expressiveness.

  • Names. Settings Fanout defines keep their FANOUT_ names. Vendor credentials and endpoints use the vendor's documented names: ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, OPENAI_API_KEY, and OPENAI_BASE_URL. Unknown FANOUT_ names still fail at startup.
  • AI chat is opt-in. FANOUT_AI_PROVIDER is empty by default, which disables chat. Set it to anthropic or openai; that provider's key is then required. Fanout reads and validates only the selected provider's variables.
  • Secrets from files. Every secret also accepts <NAME>_FILE: FANOUT_AUTH_CODE_SECRET, FANOUT_OIDC_CLIENT_SECRET, FANOUT_SMTP_PASSWORD, FANOUT_METRICS_TOKEN, ANTHROPIC_API_KEY, and OPENAI_API_KEY. Setting both forms fails, one trailing newline is stripped, an empty file fails, and errors never print file contents.
  • Listener. The binary listens on 127.0.0.1:7520 by default. The container image sets FANOUT_ADDR=:7520 and FANOUT_DATA_DIR=/var/lib/fanout/data and contains no configuration file.
  • Healthcheck. fanout healthcheck derives its URL from FANOUT_ADDR, and uses HTTPS when FANOUT_TLS_CERT_FILE is set. Wildcard binds probe 127.0.0.1. Derived probes bypass proxies. HTTPS probes of an IP address trust only the certificate in FANOUT_TLS_CERT_FILE, verified under its own SAN; hostname probes verify normally. An explicit URL argument keeps default client behavior.
  • Dotenv. The only tracked dotenv file is the generated .env.example. Every entry is commented out, so copying it into --env-file sets nothing. just run loads an optional, ignored .env with Node's parseEnv, and inherited values win. The binary never reads dotenv files.
  • Docs. README, install, operations, authentication, CONTRIBUTING, SECURITY, AGENTS.md, llms.txt, and the generated settings reference describe the environment-only model.

Breaking changes

There are no aliases or fallbacks.

  • YAML configuration and --config are removed. The image ignores a file mounted at /etc/fanout/fanout.yaml; set the matching environment variables from the settings reference instead.
  • FANOUT_AI_API_KEY and FANOUT_AI_BASE_URL are removed and fail at startup as unknown names. Use ANTHROPIC_API_KEY and ANTHROPIC_BASE_URL, or OPENAI_API_KEY and OPENAI_BASE_URL.
  • Chat requires FANOUT_AI_PROVIDER set to exactly anthropic or openai. An API key alone no longer enables chat.
  • OPENAI_BASE_URL includes /v1 (default https://api.openai.com/v1), and Fanout appends /responses. ANTHROPIC_BASE_URL excludes /v1 (default https://api.anthropic.com), and Fanout appends /v1/messages.
  • Native installs listen on loopback by default. Set FANOUT_ADDR=:7520 to accept remote connections; the container image already does.

Verification

  • just check
  • just test-race
  • just e2e
  • User-facing behavior and configuration docs are current
  • No credentials, private telemetry, host details, or enterprise-only source are included
  • Breaking configuration changes are called out above
  • Config tests cover environment resolution, strict names, _FILE secrets, and provider gating
  • Healthcheck tests cover URL derivation, wildcard and IPv6 binds, TLS, and proxy bypass
  • Provider tests assert the Anthropic and OpenAI request paths

vishr added 5 commits October 13, 2026 19:16
Use vendor credential and endpoint contracts with schema-generated public
defaults so deployment configuration has explicit ownership. Keep secrets
in runtime injection and exclude personal files from production launches.

BREAKING CHANGE: Replace the generic AI key and base URL environment names
and YAML keys with the selected provider's settings. OpenAI base URLs now
include the API version path. Retired names have no aliases or fallback.
Review fixes for the provider environment names and dotenv layout.

- AI chat is enabled only by FANOUT_AI_PROVIDER (YAML ai.provider), which
  now defaults to empty. A selected provider requires its API key, and only
  its base URL is validated. Inherited ANTHROPIC_* or OPENAI_* values from
  other tools no longer enable chat or block startup.
- Provider values must be exactly anthropic or openai; no case or space
  normalization. config.AIBaseURL is the only source of endpoint defaults.
- Generated .env and .env.production contain only environment-specific
  overrides (the listener address, declared with a local struct tag), so
  launcher-injected values no longer override --config YAML.
  .env.example lists every setting with an empty value and documents the
  default in comments.
- The launcher forwards SIGINT, SIGTERM and SIGHUP on every delivery,
  classifies secrets from each block's generated header line only, and
  accepts CRLF inventories.
- The docgen overwrite guard refuses quoted or escaped values.
- The loader is the only check of accepted environment names.
- Document the tracked .env migration and the AI configuration upgrade.
Fanout's settings are all flat, so a second configuration source added
precedence bugs without adding expressiveness.

- Remove the YAML file, the --config flag, fanout.example.yaml,
  fanout.docker.yaml and the koanf dependency. The loader resolves
  built-in defaults, then the process environment, from the schema tags.
  Environment names are unchanged; error messages name them directly.
- Every secret also accepts <NAME>_FILE (both set fails, one trailing
  newline is stripped, an empty file fails, contents are never printed).
  Vendor variables are read only for the selected AI provider.
- The binary listens on 127.0.0.1:7520 by default. The image sets
  FANOUT_ADDR=:7520 and FANOUT_DATA_DIR and bakes in no configuration file.
- fanout healthcheck derives its probe from FANOUT_ADDR, supports TLS on
  any bind address, and never routes the local probe through a proxy.
- Only the generated .env.example is tracked. just run loads an optional,
  ignored personal .env; inherited values win.
- Docs describe environment and file-based secret delivery and carry one
  upgrade table for every breaking change on this branch.
- .env.example comments out every entry, so an env file copied from it
  sets nothing and cannot replace the image's FANOUT_ADDR with an empty
  value.
- fanout healthcheck probes 127.0.0.1 for every wildcard bind, skips
  certificate verification only for derived IP-literal probes (zoned
  IPv6 included), verifies hostnames, and works out whether its URL is
  derived once, in argument parsing. It uses the exported DefaultAddr.
- One provider table drives key, base URL and file resolution,
  validation, and error names.
- The AI vendor-variable note appears only on the AI settings page.
- Delete migration and upgrade guidance and tests that named removed
  settings; no legacy paths are supported.
- Provider tests pass resolved base URLs and assert request paths.
…ames

- The authentication guide shows the OIDC community-access example as
  environment assignments and no longer describes a configuration file.
- The storage model, an OAuth comment, and the architecture and
  persistence diagrams name FANOUT_ROLLUP_SKIP_TO_LATEST,
  FANOUT_TRUSTED_PROXY_CIDRS, and FANOUT_DATA_DIR.
@vishr vishr changed the title feat(config)!: adopt provider env names and public dotenv defaults feat(config)!: configure Fanout from the environment only Oct 14, 2026
Comment thread cmd/fanout/main.go Fixed
…tificate

A derived HTTPS probe that dials an IP literal trusted any certificate,
because a bind IP is rarely in the certificate's SAN list. It now trusts
only the leaf in FANOUT_TLS_CERT_FILE and verifies it under that
certificate's first DNS or IP SAN, so the probe confirms the listener
presents the configured certificate without disabling verification.

The local TLS test asserts tls.CertificateVerificationError, which both
Linux (hostname mismatch) and macOS (unknown authority) return.
@vishr
vishr merged commit 20da787 into main Oct 14, 2026
14 checks passed
@vishr
vishr deleted the config/consumer-env-dotenv branch October 14, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants