confkit vs koanf
koanf gives you composable primitives — you wire the pipeline. confkit gives you a complete pipeline — you define the struct.
Quick Answer
confkit — opinionated, batteries-included. One Load[T] call handles sourcing, merging, defaults, validation, and redaction. Best for production services that want to get config right without assembly.
koanf — modular, composable. You assemble providers, parsers, and merge strategies yourself. Best when you have unusual pipeline requirements or need deep customization of the load process.
API Design
The fundamental difference in philosophy.
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(), confkit.FromYAML("config.yaml"), ) // cfg is *Config — typed, validated, defaults applied
k := koanf.New(".") k.Load(env.Provider("", ".", nil), nil) k.Load(file.Provider("config.yaml"), yaml.Parser()) var cfg Config k.Unmarshal("", &cfg) // No validation, no defaults, no redaction // You add those manually
Validation
type Config struct { Port int `env:"PORT" validate:"min=1,max=65535"` LogLevel string `env:"LOG_LEVEL" validate:"oneof=debug info warn error"` Region string `env:"REGION" validate:"required"` API string `env:"API_URL" validate:"http_url"` } // Validated automatically in Load()
// koanf has no validation. // Typical pattern: import "github.com/go-playground/validator/v10" var cfg Config k.Unmarshal("", &cfg) validate := validator.New() if err := validate.Struct(cfg); err != nil { // raw validator errors — not user-friendly log.Fatal(err) }
Defaults
type Config struct { Host string `env:"HOST" default:"localhost"` Port int `env:"PORT" default:"8080"` Timeout time.Duration `env:"TIMEOUT" default:"30s"` Workers int `env:"WORKERS" default:"4"` } // Defaults applied before validation if no source provides a value
// koanf has no default tag. // Add defaults via a map provider: k.Load(confmap.Provider(map[string]interface{}{} "host": "localhost", "port": 8080, "timeout": "30s", }, "."), nil) // Loaded last so higher-priority sources override it
Secret Redaction
type Config struct { DBPassword string `env:"DB_PASSWORD" secret:"true"` JWTSecret string `env:"JWT_SECRET" secret:"true"` } // Errors: DB_PASSWORD=*** (redacted) // Dump: DB_PASSWORD=*** (redacted) // Logs: DB_PASSWORD=*** (redacted)
// koanf has no concept of secret fields.
// Sensitive values appear in plain text
// in all debug output and error messages.
//
// You must implement redaction yourself
// at every logging and error call site.Error Messages
Invalid configuration:
Port
error: must be between 1 and 65535
got: 0
source: env (PORT)
DB
error: field is required
source: env (DATABASE_URL)// koanf.Unmarshal silently leaves
// zero values for missing keys.
//
// go-playground/validator errors:
// Key: 'Config.Port' Error:
// Field validation for 'Port' failed
// on the 'min' tag
// (not actionable for operators)Extensibility
Both libraries support custom sources, but via different abstractions.
type Source interface { Name() string Lookup(field FieldInfo) (Value, bool, error) } // Lookup is called per-field. // Return (value, true, nil) to provide a value. // Return (_, false, nil) to pass to next source.
type Provider interface { Load() (map[string]interface{}, error) Watch(cb func(event interface{}, err error)) error } // Returns a full document. // Merge strategy applied after.
confkit's field-level Source makes per-field priority explicit and natural. koanf's document-level Provider is more flexible for sources that return entire config trees.
Full Comparison Table
| Feature | confkit | koanf |
|---|---|---|
| API style | Generic Load[T] → struct | Compose providers, then Unmarshal |
| Typed generics | ✅ | ❌ reflect |
| Built-in validation | ✅ struct tags | ❌ manual |
| Defaults | ✅ default:"..." tag | ❌ manual |
| Secret redaction | ✅ secret:"true" | ❌ |
| Human-readable errors | ✅ | ❌ raw errors |
| Multi-source merging | ✅ field-level priority | ✅ document-level merge |
| YAML / JSON / TOML | ✅ | ✅ |
| Environment variables | ✅ | ✅ |
| CLI flags | ✅ | ✅ (pflag) |
| Vault | ✅ confkit/vault | ⚠️ community |
| AWS SSM / Secrets | ✅ confkit/aws | ⚠️ community |
| Kubernetes | ✅ confkit/k8s | ⚠️ community |
| etcd / Consul | ✅ opt-in modules | ✅ community |
| String interpolation | ✅ | ❌ |
| Schema generation | ✅ | ❌ |
| Setup overhead | minimal | moderate (wire providers) |
When to Choose
Choose confkit if…
- You want one call that does sourcing, merging, defaults, and validation
- You need validation rules and defaults declared in the struct
- You need secrets to be redacted automatically in errors and logs
- You want human-readable errors when config is wrong at deploy time
- You want typed generics instead of reflect-based unmarshaling
Choose koanf if…
- You need highly custom merge strategies at the document level
- You have unusual provider requirements not covered by confkit's Source interface
- You're comfortable wiring validation and defaults separately
- You need the koanf provider ecosystem specifically