confkit vs envconfig
envconfig does one thing: maps env vars to a struct. confkit does that plus validation, defaults, multi-source merging, and secret redaction.
Quick Answer
TL;DR
confkit — multi-source (env + YAML + cloud), validation tags, defaults tags, secret redaction. Replaces envconfig with zero boilerplate.
envconfig — environment variables only, stdlib-friendly, no validation or defaults. Best for dead-simple 12-factor services that outright refuse extra dependencies.
Sources
envconfig is env-only by design. confkit accepts sources in priority order — first match per field wins.
confkit — any source
cfg, err := confkit.Load[Config]( confkit.FromFlags(), // 1st priority confkit.FromEnv(), // 2nd priority confkit.FromYAML("cfg.yaml"), // 3rd priority vault.FromVault(addr, auth, path), )
envconfig — env only
var cfg Config if err := envconfig.Process("", &cfg); err != nil { log.Fatal(err) } // Only reads os.Getenv() // No files, no flags, no cloud
Defaults
confkit — in the struct
type Config struct { Host string `env:"HOST" default:"localhost"` Port int `env:"PORT" default:"8080"` Timeout time.Duration `env:"TIMEOUT" default:"30s"` }
envconfig — post-process
type Config struct { Host string `envconfig:"HOST"` Port int `envconfig:"PORT"` Timeout time.Duration `envconfig:"TIMEOUT"` } // Manually set defaults after Process(): if cfg.Host == "" { cfg.Host = "localhost" } if cfg.Port == 0 { cfg.Port = 8080 } if cfg.Timeout == 0 { cfg.Timeout = 30 * time.Second }
Validation
confkit — declarative tags
type Config struct { Port int `env:"PORT" validate:"min=1,max=65535"` LogLevel string `env:"LOG_LEVEL" validate:"oneof=debug info warn error"` DBURL string `env:"DATABASE_URL" validate:"required"` }
envconfig — manual code
// envconfig has no validation. // Write it yourself: if cfg.Port < 1 || cfg.Port > 65535 { return fmt.Errorf("port %d out of range", cfg.Port) } validLevels := map[string]bool{}"debug":true, "info":true if !validLevels[cfg.LogLevel] { return fmt.Errorf("invalid log level") }
Error Messages
confkit
Invalid configuration:
Port
error: must be between 1 and 65535
got: 0
source: env (PORT)
DATABASE_URL
error: field is required
source: env (DATABASE_URL)envconfig
envconfig: required key DATABASE_URL
missing value
// No field context, no source info,
// no hint on how to fix itSecret Redaction
confkit
type Config struct { DBPassword string `env:"DB_PASSWORD" secret:"true"` } // In errors: DB_PASSWORD=*** (redacted) // In dumps: DB_PASSWORD=*** (redacted)
envconfig
// No secret support.
// envconfig will print plaintext passwords
// in error messages:
// envconfig: DB_PASSWORD=hunter2 invalidMigrating from envconfig to confkit
Migration is mechanical — rename tags and change the call site:
Before (envconfig)
type Config struct { Port int `envconfig:"PORT"` DB string `envconfig:"DATABASE_URL" required:"true"` } var cfg Config envconfig.Process("", &cfg)
After (confkit)
type Config struct { Port int `env:"PORT" default:"8080" validate:"min=1,max=65535"` DB string `env:"DATABASE_URL" validate:"required" secret:"true"` } cfg, err := confkit.Load[Config](confkit.FromEnv())
Full Comparison Table
| Feature | confkit | envconfig |
|---|---|---|
| Environment variables | ✅ | ✅ |
| YAML / JSON / TOML files | ✅ | ❌ |
| CLI flags | ✅ | ❌ |
| Cloud sources | ✅ opt-in modules | ❌ |
| Defaults via struct tag | ✅ default:"..." | ❌ manual |
| Built-in validation | ✅ struct tags | ❌ manual |
| Secret redaction | ✅ secret:"true" | ❌ |
| Human-readable errors | ✅ | ⚠️ basic |
| Multi-source merging | ✅ | ❌ |
| Typed generics API | ✅ | ⚠️ reflect |
| String interpolation | ✅ | ❌ |
| Core dependencies | 2 | 0 (stdlib) |
| 12-factor compatible | ✅ | ✅ |
When to Choose
Choose confkit if…
- You want defaults declared in the struct, not in code
- You need validation (required fields, ranges, enums)
- You ever want to add a YAML file or Vault later
- You need secrets to not appear in error logs
- You want a single
Load()call to do everything
Choose envconfig if…
- You load config only from environment variables — always
- You want zero non-stdlib dependencies
- Your config is simple: no ranges, no enums, no cross-field validation
- You're already using it and it covers your needs