Skip to content
Zever
Home

Configuration

Service configuration via environment variables (no config files)

3 min readEdit on GitHub

Zever reads all service configuration from environment variables only. There are no config files (zever.yaml, .yml, .json are ignored).

Layers (later wins): defaults < .env < .env.local < process environment.

  • defaults: config.Default() — zero-infra adapters (sqlite, slog, stdhttp, memory caches, etc.)
  • .env: project-committed example + local overrides (gitignored)
  • .env.local: machine-local overrides (gitignored, higher priority)
  • process environment: highest priority, no file I/O
Terminal window
DB_ADAPTER=postgres
DB_DSN=postgres://app:secret@db:5432/app
DB_MAX_CONNS=20
CACHE_ADAPTER=redis
CACHE_ADDR=redis:6379
AUTH_ADAPTER=jwt
AUTH_JWT_SECRET=prod-secret-at-least-32-bytes-long
env, err := config.FromEnv() // reads .env, .env.local, process env
resolved, err := config.Resolve(env) // typed Config

Or in one step: config.LoadEnv() (same as FromEnv).

cfg, err := config.LoadEnv()
container.New(cfg, app.Providers()...)

Env names carry no prefix: <SERVICE>_<FIELD>, uppercased.

  • Default instance: SERVICE_FIELD (e.g., DB_ADAPTER=postgres)
  • Named instance: SERVICE__NAME__FIELD (e.g., DB__ANALYTICS__DSN=...)

Matching ignores case and _, so MAX_RETRIES matches MaxRetries. One nesting level splits at the first _: AUTH_JWT_SECRET sets Options.JWT.Secret.

Durations accept "5s" strings in env only; files store time.Duration as integer nanoseconds.

Service Default Adapter
db sqlite
cache memory
queue memory
log slog
router stdhttp
… (see config.Default())
v0.7 v0.8
zever.yaml .env + wiring.go
config.Load("") config.FromEnv() / config.LoadEnv()
ServiceConfig.Adapter wired in wiring.go via container.Provide
DB_MAXCONNS DB_MAXCONNS (same)
DB__ANALYTICS__DSN DB__ANALYTICS__DSN (same format)
zever doctor --config zever doctor (no flag)

If zever.yaml/.yml/.json exists in the working directory, config.FromEnv() prints a one-line warning:

config: zever.yaml found in working directory: ignored; see migration guide in config/README.md

The file is ignored entirely — no values are read from it.

zever new and zever add generate .env.example with every env key, default value, and comment. This is the authoritative source for what env vars each service accepts.

Raw option maps may hold secrets. (*Config).RedactedServices (built on Redact) is the sole display path: error text names services and fields only, and zever config show prints one <service> adapter=<x> header per service plus its masked options.

main.go
for name, sc := range cfg.RedactedServices() {
fmt.Println(name, sc.Adapter, sc.Options)
}

Defaults pick the zero-infra adapter per service (db sqlite, cache/queue/session memory, storage local, log slog, observability stdout, payment/billing stub, secrets env with prefix ZEVER). Two need keys: ai defaults to anthropic and requires an API key, and mailer defaults to log with a dummy localhost:25. The full table is the adapters matrix.

Unknown field error

A pre-rename flat spelling. Rename it to snake_case (e.g., maxconns → max_conns).

Unknown field under a wired service prefix

config.Resolve fails with a did-you-mean hint. Check the service’s options struct.

DB_MAXCONNS=abc returns InvalidOptionsError

A resolved field failed to parse. Set an integer. Unset variables are ignored, bad values are not.

AUTH_TOKEN does nothing

Env is best-effort: AUTH is known but the remainder matches no field. Set AUTH_JWT_SECRET.