A configuration library for Go that doesn't get in your way.
cfg := confy.New(confy.Config{})
cfg.LoadFrom(sources.NewFileSource("config.yaml", sources.FileSourceOptions{}))
port := cfg.GetInt("server.port", 8080)
debug := cfg.GetBool("debug", false)
timeout := cfg.GetDuration("timeout", 30*time.Second)Most config libraries either do too little or too much. confy sits in the middle:
- Multiple sources - files, environment variables, Consul, Kubernetes ConfigMaps
- Type-safe getters with sensible defaults
- Struct binding when you want it
- Hot reload with file watching
- Auto-discovery that finds your config files
No magic globals. No init() surprises. Just a struct you control.
go get github.com/xraph/confypackage main
import (
"github.com/xraph/confy"
"github.com/xraph/confy/sources"
)
func main() {
cfg := confy.New(confy.Config{})
source, _ := sources.NewFileSource("config.yaml", sources.FileSourceOptions{
WatchEnabled: true,
})
cfg.LoadFrom(source)
// Get values with defaults
host := cfg.GetString("database.host", "localhost")
port := cfg.GetInt("database.port", 5432)
maxConns := cfg.GetInt("database.max_connections", 10)
}confy can find your config files automatically. It searches the current directory and parent directories for config.yaml and config.local.yaml.
cfg, err := confy.AutoLoadConfy("myapp", nil)
if err != nil {
log.Fatal(err)
}This is useful for monorepos where your app might be nested several directories deep.
type DatabaseConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
MaxConns int `yaml:"max_connections" default:"10"`
IdleTimeout time.Duration `yaml:"idle_timeout" default:"5m"`
}
var dbCfg DatabaseConfig
cfg.Bind("database", &dbCfg)The default tag works when the key is missing from your config.
envSource := sources.NewEnvSource(sources.EnvSourceOptions{
Prefix: "MYAPP_",
Separator: "_",
})
cfg.LoadFrom(envSource)
// MYAPP_DATABASE_HOST becomes database.host
host := cfg.GetString("database.host")cfg.WatchChanges(func(change confy.ConfigChange) {
log.Printf("config changed: %s = %v", change.Key, change.NewValue)
})
cfg.Watch(context.Background())confy supports multiple configuration sources out of the box:
| Source | Description |
|---|---|
sources.FileSource |
YAML, JSON, TOML files |
sources.EnvSource |
Environment variables |
sources.ConsulSource |
HashiCorp Consul KV |
sources.K8sConfigMapSource |
Kubernetes ConfigMaps |
Sources have priorities. Higher priority sources override lower ones. By default:
- Base config file: 100
- Local config file: 200
- Environment variables: 300
source, err := sources.NewFileSource("config.yaml", sources.FileSourceOptions{
Priority: 100,
WatchEnabled: true,
ExpandEnvVars: true, // expands ${VAR} in values
})confy supports environment variable expansion in YAML files with bash-style default value syntax.
# config.yaml
database:
host: ${DB_HOST}
port: ${DB_PORT}export DB_HOST=localhost
export DB_PORT=5432Use bash-style syntax for default values when environment variables aren't set:
database:
# Use default if DB_HOST is unset or empty
host: ${DB_HOST:-localhost}
# Use default only if DB_PORT is unset (not if empty)
port: ${DB_PORT-5432}
# Full connection string with multiple defaults
dsn: postgres://${DB_USER:-postgres}:${DB_PASS:-postgres}@${DB_HOST:-localhost}:${DB_PORT:-5432}/${DB_NAME:-mydb}Syntax variants:
| Syntax | Behavior |
|---|---|
${VAR} or $VAR |
Standard expansion, empty string if not set |
${VAR:-default} |
Use default if VAR is unset or empty |
${VAR-default} |
Use default only if VAR is unset (not if empty) |
${VAR:=default} |
Assign and use default if VAR is unset or empty |
${VAR=default} |
Assign and use default only if VAR is unset |
Example with some variables set:
server:
address: ${SERVER_HOST:-0.0.0.0}:${SERVER_PORT:-8080}
timeout: ${TIMEOUT:-30s}export SERVER_PORT=3000
# SERVER_HOST and TIMEOUT will use defaults
# Result: address = "0.0.0.0:3000", timeout = "30s"Database connection string:
database:
dsn: ${DATABASE_DSN:-postgres://postgres:postgres@localhost:5432/testdb?sslmode=disable}This allows you to either:
- Set
DATABASE_DSNto override the entire connection string, or - Use individual environment variables with defaults for each component
Service URLs:
services:
auth: ${AUTH_URL:-http://localhost:8080}
api: ${API_URL:-http://localhost:3000}
cache: ${REDIS_URL:-redis://localhost:6379/0}confy can resolve secret references from external secret managers (AWS Secrets Manager, HashiCorp Vault, etc.):
database:
password: ${secret:db/production/password}
api_key: ${secret:services/stripe/api_key}Enable secret expansion in your file source:
source, err := sources.NewFileSource("config.yaml", sources.FileSourceOptions{
ExpandSecrets: true,
})
// Provide a secrets manager implementation
cfg.SetSecretsManager(mySecretsManager)Secret references use the format ${secret:key} where key is passed to your SecretsManager implementation.
source, err := sources.NewConsulSource(sources.ConsulSourceOptions{
Address: "localhost:8500",
Path: "myapp/config",
Token: os.Getenv("CONSUL_TOKEN"),
})source, err := sources.NewK8sConfigMapSource(sources.K8sConfigMapSourceOptions{
Namespace: "default",
ConfigMapName: "myapp-config",
Key: "config.yaml",
})Every getter has an optional default value:
cfg.GetString("key") // returns "" if missing
cfg.GetString("key", "default") // returns "default" if missing
cfg.GetInt("port", 8080)
cfg.GetBool("debug", false)
cfg.GetDuration("timeout", 10*time.Second)
cfg.GetFloat64("rate", 1.5)
cfg.GetStringSlice("hosts", []string{"localhost"})
cfg.GetSizeInBytes("max_size", 1024*1024) // supports "10MB", "1GB" stringsFor monorepos, you can scope config per application:
# config.yaml
database:
host: shared-db.internal
apps:
api:
port: 8080
database:
host: api-db.internal
worker:
concurrency: 10cfg, _ := confy.LoadConfigWithAppScope("api", logger, nil)
// Gets "api-db.internal" - app config overrides global
host := cfg.GetString("database.host")validator := confy.NewValidator(confy.ValidatorConfig{
Mode: confy.ValidationModeStrict,
})
validator.AddRule(confy.ValidationRule{
Key: "server.port",
Required: true,
Min: 1,
Max: 65535,
})
if err := cfg.Validate(); err != nil {
log.Fatal(err)
}confy includes a test implementation for unit tests:
func TestMyHandler(t *testing.T) {
cfg := confy.NewTestConfyImpl()
cfg.Set("feature.enabled", true)
cfg.Set("timeout", "5s")
handler := NewHandler(cfg)
// ...
}Or use the builder:
cfg := confy.NewTestConfigBuilder().
WithString("api.url", "http://test.local").
WithInt("retry.count", 3).
WithBool("debug", true).
Build()MIT