A Go library for type-safe configuration management using Doppler as the primary secrets and config source. Provides automatic struct mapping, hot-reload, fallback providers, validation, feature flags, and multi-tenant config patterns — all with production-grade resilience via chassis-go.
- Generic, type-safe loaders —
Loader[T]maps Doppler secrets directly into typed Go structs via struct tags - Multi-tenant support —
MultiTenantLoader[E, P]handles two-tier config (shared environment + per-project) - Hot-reload —
Watcherpolls Doppler for changes with ETag-based caching and fires callbacks on config updates - Resilient API calls — retries with exponential backoff and circuit breaking via chassis-go's
call.Client - Fallback chain —
FileProvider(local JSON) andEnvProvider(OS environment) as fallbacks when Doppler is unavailable - Validation engine — 8 built-in validators (
min,max,port,url,email,host,oneof,regex) via struct tags - Feature flags — cached flag evaluation with percentage-based rollouts and allow/block lists
- Secret redaction —
SecretValuetype that returns[REDACTED]in logs and JSON serialization - Security validation — all JSON payloads screened for prototype pollution and excessive nesting via
secval - Test utilities —
MockProvider,RecordingProvider,TestLoader[T]for easy unit testing
go get github.com/ai8future/dopplerconfigRequires Go 1.25.5+ and chassis-go v10.
type AppConfig struct {
Port int `doppler:"SERVER_PORT" default:"8080" validate:"port"`
DBUrl string `doppler:"DATABASE_URL" required:"true" validate:"url"`
Password SecretValue `doppler:"DB_PASSWORD" secret:"true"`
LogLevel string `doppler:"LOG_LEVEL" default:"info" validate:"oneof=debug|info|warn|error"`
Hosts []string `doppler:"ALLOWED_HOSTS"`
Timeout time.Duration `doppler:"REQUEST_TIMEOUT" default:"30s"`
}bootstrap := dopplerconfig.LoadBootstrapFromEnv()
loader, err := dopplerconfig.NewLoader[AppConfig](bootstrap)
if err != nil {
log.Fatal(err)
}
defer loader.Close()
cfg, err := loader.Load(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("Port:", cfg.Port)
fmt.Println("Password:", cfg.Password) // prints "[REDACTED]"loader.OnChange(func(old, new *AppConfig) {
log.Println("Config changed, new port:", new.Port)
})
stop := dopplerconfig.Watch(ctx, loader,
dopplerconfig.WithWatchInterval[AppConfig](30 * time.Second),
)
defer stop()type EnvConfig struct {
MaxPoolSize int `doppler:"MAX_POOL_SIZE" default:"10"`
}
type ProjectConfig struct {
APIKey string `doppler:"API_KEY" required:"true"`
Endpoint string `doppler:"ENDPOINT" validate:"url"`
}
mtLoader, err := dopplerconfig.NewMultiTenantLoader[EnvConfig, ProjectConfig](bootstrap)
if err != nil {
log.Fatal(err)
}
env, _ := mtLoader.LoadEnv(ctx)
projects, _ := mtLoader.LoadAllProjects(ctx, []string{"proj-a", "proj-b"})| Tag | Purpose | Example |
|---|---|---|
doppler |
Doppler secret key name | doppler:"DATABASE_URL" |
env |
Fallback key name (chassis-go compat) | env:"DATABASE_URL" |
default |
Default value if key is absent | default:"8080" |
required |
Fail if key is missing or empty | required:"true" |
secret |
Marks sensitive fields | secret:"true" |
validate |
Validation rules (comma-separated) | validate:"port,min=1000" |
description |
Documentation for the field | description:"gRPC port" |
Tag priority: doppler > env > field name.
| Rule | Syntax | Description |
|---|---|---|
min |
validate:"min=10" |
Minimum value (int) or length (string) |
max |
validate:"max=100" |
Maximum value or length |
port |
validate:"port" |
Valid port number (1-65535) |
url |
validate:"url" |
Parseable URI |
host |
validate:"host" |
RFC 1123 hostname or IP, optional port |
email |
validate:"email" |
Valid email format |
oneof |
validate:"oneof=a|b|c" |
Must match one of the pipe-delimited values |
regex |
validate:"regex=^[a-z]+$" |
Must match the regex pattern |
| Variable | Purpose | Default |
|---|---|---|
DOPPLER_TOKEN |
Doppler service or personal token | (required if no fallback) |
DOPPLER_PROJECT |
Doppler project name | (optional with service tokens) |
DOPPLER_CONFIG |
Config name (dev/stg/prd) | (optional with service tokens) |
DOPPLER_FALLBACK_PATH |
Path to local JSON fallback file | (none) |
DOPPLER_WATCH_ENABLED |
Enable hot-reload polling | false |
DOPPLER_FAILURE_POLICY |
fail, fallback, or warn |
fallback |
| Policy | Behavior |
|---|---|
fail |
Return error immediately if Doppler is unavailable (strict) |
fallback |
Use fallback file/env if Doppler is unavailable (default) |
warn |
Log warning and use struct default tags only (lenient) |
All config sources implement the Provider interface:
type Provider interface {
Fetch(ctx context.Context) (map[string]string, error)
FetchProject(ctx context.Context, project, config string) (map[string]string, error)
Name() string
Close() error
}Built-in providers:
| Provider | Description |
|---|---|
DopplerProvider |
Live Doppler API with retries, circuit breaking, and ETag caching |
FileProvider |
Local JSON file (supports nested JSON with automatic flattening) |
EnvProvider |
OS environment variables with optional prefix |
MockProvider |
In-memory provider for tests |
RecordingProvider |
Decorator that records all fetch calls for test assertions |
DopplerProvider uses chassis-go's call.Client under the hood:
- Retries: 3 attempts with exponential backoff (1s, 2s, 4s)
- Circuit breaker: Opens after 5 consecutive failures, stays open for 30 seconds
- ETag caching:
304 Not Modifiedresponses return cached values with zero JSON parsing - Timeout: 30-second per-request timeout
- Health check:
HealthCheck(provider)returns a function suitable for health check endpoints
provider, _ := dopplerconfig.NewDopplerProvider(token, project, config,
dopplerconfig.WithCallOptions(
call.WithTimeout(15 * time.Second),
call.WithRetry(5, 2 * time.Second),
),
)
// Check circuit state
state := provider.CircuitState() // call.StateClosed, StateOpen, or StateHalfOpenflags := dopplerconfig.NewFeatureFlags(secretValues, "FF_")
if flags.IsEnabled("DARK_MODE") {
// Feature is on
}
maxRetries := flags.GetInt("MAX_RETRIES", 3)Percentage-based rollouts:
rollout := &dopplerconfig.RolloutConfig{
Percentage: 25,
AllowList: []string{"beta-user-1"},
BlockList: []string{"excluded-user"},
}
if rollout.ShouldEnable(userID, hashFunc) {
// Enabled for ~25% of users + allow list
}func TestMyService(t *testing.T) {
loader, mock := dopplerconfig.TestLoader[AppConfig](map[string]string{
"SERVER_PORT": "9090",
"DATABASE_URL": "postgres://localhost/test",
"DB_PASSWORD": "secret",
})
cfg, err := loader.Load(context.Background())
if err != nil {
t.Fatal(err)
}
// Update config mid-test
mock.SetValue("SERVER_PORT", "9091")
cfg, _ = loader.Reload(context.Background())
}Consumer Service
│
▼
Loader[T] / MultiTenantLoader[E,P] ← Generic loaders with callbacks
│
▼
Provider interface ← Pluggable data sources
┌──┴──┐
▼ ▼
Doppler File/Env ← Primary + fallback
│
▼
chassis-go call.Client ← Retries + circuit breaker
│
▼
Doppler REST API (v3)
The reflection-based unmarshaller handles:
- Primitives:
string,int,int8–int64,uint–uint64,float32,float64,bool time.Duration(e.g.,"30s","5m")SecretValue(redacted in logs/JSON)- Slices:
[]string,[]int,[]bool(comma-separated values) - Nested and embedded structs
Private — internal infrastructure module.