Documentation

Everything you need to load, validate, and manage typed configuration in Go.

Getting Started

Install confkit and load your first typed configuration in under a minute.

Installation

terminal
go get github.com/MimoJanra/confkit@v0.9.0

Go 1.24 or later required.

30-Second Example

Define a struct with tags describing where values come from. That's all the configuration you need.

main.go
package main

import (
    "log"
    "time"
    "github.com/MimoJanra/confkit"
)

type Config struct {
    Host    string        `env:"HOST"     default:"localhost"`
    Port    int           `env:"PORT"     default:"8080"      validate:"min=1,max=65535"`
    Timeout time.Duration `env:"TIMEOUT"  default:"30s"`
    DB      struct {
        DSN      string `env:"DSN"       validate:"required" secret:"true"`
        MaxConns int    `env:"MAX_CONNS" default:"10"        validate:"min=1,max=100"`
    } `prefix:"DB_"`
}

func main() {
    cfg, err := confkit.Load[Config](
        confkit.FromFlags(),             // highest priority — checked first
        confkit.FromEnv(),               // overrides file
        confkit.FromYAML("config.yaml"), // fallback
    )
    if err != nil {
        log.Fatal(confkit.Explain(err))
    }
    log.Printf("listening on %s:%d", cfg.Host, cfg.Port)
}

Multiple Sources & Precedence

Sources use first-wins semantics. The first source to provide a value for a field wins. List your highest-priority sources first.

cfg, err := confkit.Load[Config](
    confkit.FromEnv(),                    // 1st: runtime overrides
    confkit.FromYAML("config.yaml"),     // 2nd: env-specific config
    confkit.FromYAML("defaults.yaml"),   // 3rd: base defaults (fallback)
)

Human-Readable Errors

When validation fails, confkit.Explain(err) formats the error for humans:

Invalid configuration:

  Port
    error: must be between 1 and 65535, got 99999
    source: env (PORT)

  DB.DSN
    error: field is required
    source: env (DB_DSN)
No secret values are ever shown in error messages — fields tagged secret:"true" always display as <redacted>.

Configuration Sources

confkit loads from any number of sources. Pass them in priority order — first source per field wins.

Environment Variables — FromEnv()

type Config struct {
    Port     int    `env:"PORT"         default:"8080"`
    Database string `env:"DATABASE_URL"  validate:"required"`
}
cfg, err := confkit.Load[Config](confkit.FromEnv())

Env prefix for nested structs

type Config struct {
    DB struct {
        Host string `env:"HOST" default:"localhost"`
        Port int    `env:"PORT" default:"5432"`
    } `prefix:"DB_"`  // reads DB_HOST, DB_PORT
}

YAML — FromYAML(path)

cfg, err := confkit.Load[Config](confkit.FromYAML("config.yaml"))

JSON — FromJSON(path)

cfg, err := confkit.Load[Config](confkit.FromJSON("config.json"))

TOML — FromTOML(path)

cfg, err := confkit.Load[Config](confkit.FromTOML("config.toml"))

Multi-File Merging — FromYAMLFiles / FromJSONFiles / FromTOMLFiles

Merge multiple files of the same format. The first file to provide a value wins; nested maps are deep-merged.

cfg, err := confkit.Load[Config](
    confkit.FromYAMLFiles(
        "config/base.yaml",       // shipped defaults
        "config/production.yaml", // environment overlay
        "config/local.yaml",      // developer overrides (git-ignored)
    ),
    confkit.FromEnv(), // env vars override everything
)

CLI Flags — FromFlags()

Flags are converted to kebab-case. Supports --key=value, --key value, -k value, and bare --flag for booleans.

type Config struct {
    Verbose  bool   `flag:"verbose" short:"v"`
    Output   string `flag:"output"  short:"o" default:"stdout"`
    InputDir string `flag:"input"   validate:"required"`
}
// ./mytool -v -o file.txt --input /data

Kubernetes ConfigMap — k8s.FromKubernetesConfigMap

import "github.com/MimoJanra/confkit/k8s"

cfg, err := confkit.Load[Config](
    k8s.FromKubernetesConfigMap("default", "app-config"),
)

HashiCorp Vault — vault.FromVault

import (
    "os"
    "github.com/MimoJanra/confkit/vault"
)

auth := vault.VaultTokenAuth(os.Getenv("VAULT_TOKEN"))
// Also: vault.VaultAppRoleAuth(roleID, secretID)
// Also: vault.VaultKubernetesAuth(role, jwt)

cfg, err := confkit.Load[Config](
    vault.FromVault("https://vault.example.com", auth, "/secret/myapp"),
)

AWS SSM Parameter Store — aws.FromAWSSSMParameterStore

import "github.com/MimoJanra/confkit/aws"

cfg, err := confkit.Load[Config](
    aws.FromAWSSSMParameterStore("/prod/app/config"),
)

AWS Secrets Manager — aws.FromAWSSecretsManager

cfg, err := confkit.Load[Config](
    aws.FromAWSSecretsManager("prod/app-secrets"),
)

Consul KV — consul.FromConsul

import "github.com/MimoJanra/confkit/consul"

cfg, err := confkit.Load[Config](
    consul.FromConsulWithOptions("consul.example.com:8500", "token", "dc1"),
)

etcd v3 — etcd.FromEtcd

import "github.com/MimoJanra/confkit/etcd"

cfg, err := confkit.Load[Config](
    etcd.FromEtcdWithPrefix(
        []string{"etcd1.example.com:2379", "etcd2.example.com:2379"},
        "/myapp",
    ),
)

Custom Sources

Implement the Source interface — just two methods:

type MySource struct{ data map[string]string }

func (s *MySource) Name() string { return "custom" }

func (s *MySource) Lookup(ctx context.Context, field *confkit.FieldInfo) (any, bool, error) {
    val, ok := s.data[field.Path]
    return val, ok, nil
}

cfg, err := confkit.Load[Config](&MySource{ data: map[string]string{"Port": "8080"} })

Validation

Validation rules live in struct tags and are applied after all sources are loaded. Rules are comma-separated, all must pass.

Built-in Rules

RuleTypesDescription
requiredanyNon-zero value required
min=Nint, floatValue ≥ N
min=NstringLength ≥ N characters
max=Nint, floatValue ≤ N
max=NstringLength ≤ N characters
oneof=a b cstringMust equal one space-separated option
emailstringValid email address
urlstringValid URL (any scheme)
http_urlstringValid HTTP/HTTPS URL
ip / ipv4 / ipv6stringValid IP address
uuidstringValid UUID v1–v5
hostnamestringValid hostname per RFC 1123
portint, stringValid port 1–65535
regex=patternstringMust match the regular expression
len=NstringMust be exactly N characters
contains=strstringMust contain the substring
startswith=strstringMust start with the prefix
endswith=strstringMust end with the suffix
alphastringLetters only
alphanumstringLetters and digits only
numericstringDigits only
lowercase / uppercasestringAll lower- or uppercase
notemptystringNon-blank (non-whitespace) required

Example — combining rules

type Config struct {
    Port        int    `env:"PORT"      validate:"required,port"`
    AdminEmail  string `env:"EMAIL"     validate:"required,email"`
    APIKey      string `env:"API_KEY"   validate:"required,len=32,alphanum" secret:"true"`
    Environment string `env:"ENV"       validate:"oneof=dev staging prod"`
    ServiceURL  string `env:"SVC_URL"   validate:"http_url"`
    ServiceID   string `env:"SVC_ID"    validate:"uuid"`
    Region      string `env:"REGION"    validate:"regex=^[a-z]+-[a-z]+-[0-9]+$"`
}

Custom Validators

Register named validators per load call — no global state:

cfg, err := confkit.LoadWithOptions[Config](
    confkit.WithSource(confkit.FromEnv()),
    confkit.WithValidator("port-range", func(v reflect.Value) error {
        n := v.Int()
        if n < 1024 || n > 49151 {
            return fmt.Errorf("must be a registered port (1024–49151), got %d", n)
        }
        return nil
    },
)
// Use in tag: validate:"port-range"

Cross-Field Validation (Model Validators)

cfg, err := confkit.LoadWithOptions[TLSConfig](
    confkit.WithSource(confkit.FromEnv()),
    confkit.WithModelValidator(func(cfg *TLSConfig) error {
        if cfg.Enabled && cfg.CertPath == "" {
            return fmt.Errorf("tls_cert_path required when tls_enabled is true")
        }
        return nil
    },
)

Programmatic Error Inspection

cfg, err := confkit.Load[Config](confkit.FromEnv())
if err != nil {
    report := err.(*confkit.ErrorReport)
    for _, fieldErr := range report.Errors {
        fmt.Printf("Field: %s\n", fieldErr.Path)
        fmt.Printf("Error: %s\n", fieldErr.Message)
        fmt.Printf("Rule:  %s\n", fieldErr.Rule)
        fmt.Printf("Value: %s\n", fieldErr.Value) // empty if Secret=true
    }
}

Secret Redaction

Mark any field with secret:"true" and its value will never appear in errors, config dumps, or logs.

Marking Secrets

type Config struct {
    Username string `env:"DB_USER"`
    Password string `env:"DB_PASSWORD" secret:"true"`
    APIKey   string `env:"API_KEY"     secret:"true" validate:"required"`
}

In Error Messages

Invalid configuration:

  APIKey
    error: field is required
    source: env (API_KEY)
    value: <redacted>   ← never shows actual value

Safe Config Dump

fields := confkit.ScanFields(cfg)
dump, _ := confkit.DumpConfig(cfg, fields)
log.Println(dump)
// {"Username":"admin","Password":"***REDACTED***","APIKey":"***REDACTED***"}

Safe Logging Practices

// ✅ Safe — secrets are redacted
if err != nil { log.Error(confkit.Explain(err)) }

fields := confkit.ScanFields(cfg)
dump, _ := confkit.DumpConfig(cfg, fields)
log.Info("Config loaded", "config", dump) // ✅ Safe

// ❌ Never do this — exposes secrets
log.Printf("Config: %+v", cfg)
confkit guarantees redaction in: error messages via Explain(), config dumps via DumpConfig(), and programmatic error inspection (FieldError.Value is empty for secrets).

Defaults

Defaults are applied after all sources are loaded — only if a field wasn't set by any source. They are the lowest priority.

Basic Defaults

type Config struct {
    Host    string        `env:"HOST"    default:"localhost"`
    Port    int           `env:"PORT"    default:"8080"`
    Timeout time.Duration `env:"TIMEOUT" default:"30s"`
    Debug   bool          `env:"DEBUG"   default:"false"`
}

Type Coercion

TypeExample defaultNotes
int / uint / floatdefault:"8080"Decimal number
booldefault:"true"true, false, yes, no, 1, 0
time.Durationdefault:"30s"5ms, 30s, 1m30s, 2h
stringdefault:"localhost"As-is
[]string / []intdefault:"a,b,c"Comma-separated

Defaults & Validation

Defaults must satisfy validation rules. A default that violates validation is a programmer error — confkit will panic at startup to catch it early.

// ✅ Good — default passes validation
Port int `env:"PORT" default:"8080" validate:"min=1,max=65535"`

// ❌ Bad — panic at startup: default violates max=65535
Port int `env:"PORT" default:"99999" validate:"min=1,max=65535"`

Priority Order

  1. CLI flags (highest priority)
  2. Environment variables
  3. Config files (YAML / JSON / TOML)
  4. Defaults (lowest priority)

Hot Reload

Watch a config file for changes and reload without restarting your application.

Basic Usage

cfg, watcher, err := confkit.LoadWithWatcher[Config]("config.yaml",
    confkit.FromEnv(),
    confkit.FromYAML("config.yaml"),
)
if err != nil {
    log.Fatal(err)
}

watcher.AddListener(func(oldCfg, newCfg any, err error) {
    if err != nil {
        log.Printf("Reload failed: %v", err)
        return // old config is preserved on error
    }
    log.Println("Config reloaded")
    cfg = newCfg.(Config)
})

watcher.SetPollInterval(2 * time.Second) // default: 500ms
watcher.Start()
defer watcher.Stop()
If a reload fails (invalid YAML, validation error), the old config is preserved and the listener receives a non-nil error.

Thread-Safe Access

type Server struct {
    mu  sync.RWMutex
    cfg Config
}

func (s *Server) Config() Config {
    s.mu.RLock()
    defer s.mu.RUnlock()
    return s.cfg
}

watcher.AddListener(func(_, new any, err error) {
    if err != nil { return }
    s.mu.Lock()
    s.cfg = new.(Config)
    s.mu.Unlock()
})

Schema Generation

Generate JSON Schema, Markdown documentation, and CLI help text automatically from your struct.

JSON Schema

import "github.com/MimoJanra/confkit/schema"

type Config struct {
    Port     int    `env:"PORT" default:"8080" validate:"min=1,max=65535" help:"Server port"`
    Host     string `env:"HOST" default:"localhost" help:"Server hostname"`
    Database string `env:"DATABASE_URL" validate:"required" secret:"true" help:"Connection URL"`
}

s, err := schema.GenerateSchema[Config]()
data, _ := json.MarshalIndent(s, "", "  ")
fmt.Println(string(data))

Markdown Reference Table

md := schema.GenerateMarkdown[Config]()
fmt.Println(md)

CLI Help Text

help := schema.GenerateCLIHelp[Config]()
fmt.Println(help)

String Interpolation

Values can reference other fields or env vars using ${NAME}. Resolution order: config fields first, then OS environment. Circular references are detected.

type Config struct {
    Host    string `env:"HOST"     default:"localhost"`
    Port    int    `env:"PORT"     default:"8080"`
    BaseURL string `env:"BASE_URL" default:"http://${ HOST}:${ PORT}/api"`
}
// BaseURL → "http://localhost:8080/api" when no overrides

Supported Types

TypeParsed from
stringas-is
int / int8 / int16 / int32 / int64decimal
uint / uint8 / uint16 / uint32 / uint64decimal
float32 / float64decimal
booltrue false 1 0 yes no
time.Duration"5s" "1m30s" "2h"
[]stringcomma-separated "a,b,c"
[]intcomma-separated "1,2,3"
map[string]string / map[string]intKEY=val,KEY2=val2