confkit vs koanf

koanf gives you composable primitives — you wire the pipeline. confkit gives you a complete pipeline — you define the struct.

Quick Answer

TL;DR

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.

confkit — struct-first
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
koanf — provider-first
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

confkit — built-in
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 — manual integration
// 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

confkit — struct tags
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 — manual
// 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

confkit — automatic
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 — not supported
// 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

confkit — structured + human-readable
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 — raw unmarshal errors
// 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.

confkit — field-level Source
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.
koanf — document-level Provider
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

Featureconfkitkoanf
API styleGeneric Load[T] → structCompose providers, then Unmarshal
Typed generics❌ reflect
Built-in validation✅ struct tags❌ manual
Defaultsdefault:"..." tag❌ manual
Secret redactionsecret:"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 overheadminimalmoderate (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