Vukem has no stringly lookups such as config("app.name"). Configuration is read once at boot into typed structs; invalid values abort boot with all problems listed, not only the first one.
.env files#
The config package ships a small, standard-library-only .env parser. Values are resolved with this precedence, highest first:
- The real process environment
.env.<APP_ENV>.local.env.local(skipped whenAPP_ENV=test, so tests are reproducible).env.<APP_ENV>.env
APP_NAME=Blog
APP_ENV=development
# Written by vukem key:generate
APP_KEY=base64:your-32-byte-key-in-base64
APP_URL=http://localhost:3000
# ${VAR} interpolation
ASSET_URL=${APP_URL}/build
# Read the value from a file (Docker and Kubernetes secrets)
DB_PASSWORD=@file:/run/secrets/db
# Quotes and escapes
GREETING="Hello\nWorld"
export LOG_LEVEL=debugSupported syntax: KEY=value, export KEY=value, single and double quotes, \n escapes inside double quotes (values may span lines), # comments, ${OTHER} interpolation and the @file: prefix. The @ keeps SQLite DSNs such as file:storage/app.db untouched.
Per-environment files override .env:
APP_NAME=Blog
APP_ENV=development
APP_KEY=
APP_URL=http://localhost:3000Core settings#
vukem.New loads the core vukem.Config:
| Variable | Default | Notes |
|---|---|---|
APP_NAME | Vukem | |
APP_ENV | development | development, production or test |
APP_DEBUG | false | Must be false when APP_ENV=production (boot error otherwise) |
APP_URL | http://localhost:3000 | Public origin of the app |
APP_KEY | — | Required in production (and for any key-derived feature), at least 32 bytes, base64: prefix optional |
HOST | localhost | Listen host; 0.0.0.0 for all IPv4 interfaces, :: for IPv6 (dual-stack on most systems) |
PORT | 3000 | Listen port, 0–65535 |
SHUTDOWN_TIMEOUT | 15s | Graceful shutdown budget |
TRUSTED_PROXIES | empty (trust nobody) | Comma-separated CIDRs or IPs whose X-Forwarded-For sets c.IP(); an invalid entry is a boot error (Deploy) |
CLIENT_IP_HEADER | empty | Read the client IP from this single-IP header (e.g. X-Real-IP) instead of X-Forwarded-For, only from TRUSTED_PROXIES peers |
TRUST_FORWARDED_PROTO | false | Treat X-Forwarded-Proto: https from a trusted proxy as HTTPS (HSTS); requires TRUSTED_PROXIES |
HSTS_FORCE | false | Send HSTS on every response in production when the TLS proxy's range cannot be trusted (trade-offs); validated in every environment (requires an https:// APP_URL), applied only in production (a boot warning otherwise) |
LOG_LEVEL | info | debug, info, warn, error |
LOG_FORMAT | text (json in production) | text or json |
LOG_DEDUP_WINDOW | 1m in production, off elsewhere | Repeated identical errors become one repeated summary per window; 0 disables it |
Typed module config#
Fill your own structs with config.Load[T] using env and default tags. Fields of nested structs without an env tag are filled recursively.
type Mail struct {
Host string `env:"MAIL_HOST" default:"localhost"`
Port int `env:"MAIL_PORT" default:"1025"`
From string `env:"MAIL_FROM"`
Password config.Secret `env:"MAIL_PASSWORD"`
Timeout time.Duration `env:"MAIL_TIMEOUT" default:"10s"`
Cc []string `env:"MAIL_CC"` // comma-separated
}
func LoadMail(app *vukem.App) (Mail, error) {
return config.Load[Mail](app.Source) // every conversion error, joined
}Supported field types: string, bool, ints, uints, floats, time.Duration, []string (comma-separated), *url.URL, config.Secret and any encoding.TextUnmarshaler.
Testing with a fake environment#
Tests inject configuration instead of calling os.Setenv:
src := config.MapSource(map[string]string{
"APP_KEY": config.GenerateKey(),
"APP_ENV": "test",
})
app, err := vukem.New(vukem.WithSource(src))vukem.WithEnv(fsys, getenv) loads .env files from any fs.FS (for example fstest.MapFS) with a custom getenv.
Secret#
config.Secret holds sensitive values. Every formatting path prints [redacted]: fmt (%v, %s, %#v), encoding/json, encoding.TextMarshaler and log/slog.
cfg.Password.Reveal() // the real value, only where it is used
fmt.Println(cfg.Password) // [redacted]
app.Log.Info("mail", "cfg", cfg) // password logged as [redacted]APP_KEY#
- Required.
vukem key:generatewrites one to.env(creating it from.env.examplewhen missing);--showprints a key instead and--forcereplaces an existing one, which makes current sessions and encrypted data unreadable. Where only the compiled app exists (Docker, CI),./app key:generateprints a new key without booting the app or writing any file; see CLI. In code,config.GenerateKey()returns 32 random bytes fromcrypto/rand,base64:-encoded. - Required in production and whenever
APP_ENVis unset or empty. Only with an explicitAPP_ENV=developmentortestdoes an app boot without it (development logs one warning), and every key-derived feature fails closed on use: cookie sessions (SESSION_DRIVER=cookie, the default, soDefaultMiddlewareis still a boot error),app.Crypt(nil;DecryptandSetCookiereturncrypt.ErrNoKey,Encryptpanics),app.Key.Derive(panics),app.DeriveKey(returnsconfig.ErrKeyMissing), encrypted ORM columns (orm.ErrNoEncrypter) and the maintenance bypass cookie (disabled). An API-only app with token auth and no encryption runs without a key. A malformed key is always a boot error. - The decoded key is available as
app.Key. Never use it directly: derive a purpose-specific key with HKDF-SHA256 so cookies, sessions and encryption never share key material.
sessionKey := app.Key.Derive("session", 32) // HKDF info "vukem:session"
signer, err := app.DeriveKey("signed.urls", 32) // config.ErrKeyMissing instead of a panicAPP_PREVIOUS_KEYS lists former keys (comma-separated, same base64: format). They only open or verify what they sealed or signed (encrypted cookies and columns, queued mail, cookie sessions, the maintenance bypass, and signed URLs and social state through app.DerivePreviousKeys); everything new uses APP_KEY. vukem key:rotate moves the current key there and writes a new one; see Encryption.
Secure defaults#
| Check | Behaviour |
|---|---|
Missing APP_KEY in production or with APP_ENV unset, malformed APP_KEY anywhere | Boot error |
Missing APP_KEY with explicit APP_ENV=development/test | Boots; key-derived features fail closed on use |
APP_DEBUG=true with APP_ENV=production | Boot error |
PORT out of range | Boot error |
Invalid TRUSTED_PROXIES entry; TRUST_FORWARDED_PROTO or CLIENT_IP_HEADER without TRUSTED_PROXIES; an invalid or list CLIENT_IP_HEADER; HSTS_FORCE without an https:// APP_URL | Boot error |
| Debug error details | Only when Services.Debug is set; never in production |