Configuration Precedence
Configuration Precedence
How Jan Server loads configuration values and decides which source wins.
Quick Summary
| Priority | Source | Notes |
|---|---|---|
| 600 | CLI flags (future) | Planned for jan-cli integrations |
| 500 | Environment variables | Secrets, overrides, CI/CD |
| 300 | config/environments/*.yaml | Per-environment overrides |
| 200 | config/defaults.yaml | Generated defaults (make config-generate) |
| 100 | Struct tags (envDefault) | Absolute fallback values |
Rule: the highest priority number wins every time. Example: POSTGRES_PORT=5433 (priority 500) beats port: 5432 in defaults.yaml (priority 200).
Configuration Sources
1. StructDefaultSource (Priority 100)
Lowest priority. These are the hardcoded defaults embedded in Go struct tags in pkg/config/types.go.
type PostgresConfig struct {
Port int `yaml:"port" json:"port" env:"POSTGRES_PORT" envDefault:"5432"`
// ^^^^^^^^^^^^^^^^
// Priority 100
}
// NOTE: Never store secrets in envDefault tagsWhen to use: Never set directly - these are fallback defaults.
Example:
// From types.go
Host string `envDefault:"api-db"` // Priority 100: "api-db"2. YAMLDefaultSource (Priority 200)
Second priority. Auto-generated config/defaults.yaml created by make config-generate.
infrastructure:
database:
postgres:
host: "api-db" # Priority 200
port: 5432 # Priority 200
user: "jan_user"When to use: Baseline configuration for every environment. Generated via make config-generate; never edit manually.
Example:
# Generate defaults.yaml
make config-generate
# This creates config/defaults.yaml with all default values3. YAMLEnvSource (Priority 300)
Third priority. Environment-specific configuration files in config/environments/*.yaml.
# config/environments/production.yaml
infrastructure:
database:
postgres:
host: "prod-db.example.com" # Priority 300: overrides defaults.yaml
port: 5432 # Priority 300: same as default, redundant
max_connections: 500 # Priority 300: production tuningWhen to use:
- Production/staging-specific settings
- Infrastructure endpoints that differ per environment
- Feature flags per environment
File naming:
config/environments/development.yamlconfig/environments/staging.yamlconfig/environments/production.yaml
Example:
# Load development environment
loader:= config.NewConfigLoader("development", "config/defaults.yaml")
cfg, err:= loader.Load(context.Background())
# Loads in order:
# 1. Struct defaults (100)
# 2. config/defaults.yaml (200)
# 3. config/environments/development.yaml (300) <- Environment-specific
# 4. Environment variables (500)4. EnvVarSource (Priority 500)
Fourth priority. System environment variables with names matching env struct tags.
# Priority 500: Overrides everything except CLI flags
export POSTGRES_HOST=override-db
export POSTGRES_PORT=5433
export AUTO_MIGRATE=falseWhen to use:
- Docker/Kubernetes deployments
- CI/CD pipelines
- Local development overrides
- Secrets and sensitive values (managed by DevOps)
Tag mapping:
// From types.go
Host string `env:"POSTGRES_HOST"` // Set with: export POSTGRES_HOST=value
Port int `env:"POSTGRES_PORT"` // Set with: export POSTGRES_PORT=5433Example:
# Override database host for local testing
export POSTGRES_HOST=localhost
export POSTGRES_PORT=5433
# Run service - will use localhost:5433 instead of config values
./llm-api5. CLI Flags (Priority 600) - Planned
Highest priority. Command-line flags (not yet implemented, planned for Sprint 7+).
# Planned for future
./llm-api --db-host=emergency-db --db-port=5434When to use:
- Emergency overrides
- One-time testing
- Troubleshooting in production
Conflict Resolution
Merge Strategy
The configuration loader uses a non-zero override strategy:
- Start with an empty
Configstruct - Load sources in ascending priority order (100 -> 200 -> 300 -> 500)
- For each source:
- Load configuration values
- Only non-zero values from the source override existing values
- Zero values are skipped (don't override with empty strings, 0, false)
Example: Port Precedence
// Initial state (empty Config)
Port: 0
// Load StructDefaultSource (Priority 100)
Port: 5432 // From envDefault tag
// Load YAMLDefaultSource (Priority 200)
Port: 5432 // defaults.yaml matches struct default, no change
// Load YAMLEnvSource (Priority 300)
Port: 5433 // production.yaml overrides to 5433
// Load EnvVarSource (Priority 500)
Port: 5434 // POSTGRES_PORT env var wins
// Final value: 5434Provenance Tracking
The loader tracks which source provided each final value:
loader:= config.NewConfigLoader("production", "config/defaults.yaml")
cfg, _:= loader.Load(ctx)
// Print configuration sources
fmt.Println(loader.Provenance())
// Output:
// Configuration Sources (priority order):
// [100] struct-defaults
// [200] yaml-defaults
// [300] yaml-env-production
// [500] env-vars
//
// Loaded 127 configuration valuesDebug: Finding Value Origin
To debug where a specific value came from:
info, err:= loader.Provenance("infrastructure.database.postgres.port")
if err == nil {
fmt.Printf("Port came from: %s (priority %d)\n", info.Source, info.Priority)
// Output: Port came from: env-vars (priority 500)
}Common Scenarios
Scenario 1: Development Override
Goal: Use localhost database for local development
# Set environment variables
export POSTGRES_HOST=localhost
export POSTGRES_PORT=5432
# Run service
go run./cmd/serverResult:
POSTGRES_HOST: localhost (priority 500, env var)POSTGRES_PORT: 5432 (priority 500, env var)- All other settings: from defaults.yaml (priority 200)
Scenario 2: Production Deployment
Goal: Use production database with secrets
# config/environments/production.yaml (priority 300)
infrastructure:
database:
postgres:
host: "prod-db.company.com"
ssl_mode: "require"# Kubernetes secret (priority 500)
export POSTGRES_PASSWORD=<secret-from-vault>
export POSTGRES_USER=<secret-from-vault>
# Run with production environment
./llm-api --environment=productionResult:
host: prod-db.company.com (priority 300, environment YAML)ssl_mode: require (priority 300, environment YAML)user,password: from env vars (priority 500, secrets)- All other settings: from defaults.yaml (priority 200)
Scenario 3: Temporary Override
Goal: Test with different port without changing config
# One-time override for testing
export POSTGRES_PORT=9999
# Run test
go test./...
# Unset when done
unset POSTGRES_PORTResult:
POSTGRES_PORT: 9999 (priority 500) during test- All other services still use 5432 from defaults
Data Type Handling
Strings
export POSTGRES_HOST=localhost # Simple string
export KEYCLOAK_REALM=jan # No quotes neededIntegers
export POSTGRES_PORT=5433 # Parsed as int
export POSTGRES_MAX_CONNECTIONS=200Booleans
export AUTO_MIGRATE=true # Accepts: true, false, 1, 0
export OTEL_ENABLED=falseDurations
export DB_CONN_MAX_LIFETIME=45m # Go duration format
export MEDIA_S3_PRESIGN_TTL=10m # Supports: ns, us, ms, s, m, hSlices (comma-separated)
export KEYCLOAK_FEATURES=token-exchange,preview,admin-api
# Parsed as: []string{"token-exchange", "preview", "admin-api"}Best Practices
OK DO
- Use environment variables for secrets
export POSTGRES_PASSWORD=$VAULT_SECRET
export AWS_ACCESS_KEY_ID=$AWS_KEY- Use environment YAML for per-environment infrastructure
# config/environments/staging.yaml
infrastructure:
database:
postgres:
host: "staging-db.internal"- Keep defaults.yaml comprehensive
# Regenerate after adding new config fields
make config-generate- Document precedence in comments
# config/environments/production.yaml
infrastructure:
database:
postgres:
# Override for production (priority 300)
# Can still be overridden by POSTGRES_HOST env var (priority 500)
host: "prod-db.company.com"[X] DON'T
- Don't manually edit defaults.yaml
# [X] WRONG: Manual edits will be overwritten
vim config/defaults.yaml
# OK CORRECT: Edit types.go and regenerate
vim pkg/config/types.go
make config-generate- Don't put secrets in YAML files
# [X] WRONG: Secret in version control
infrastructure:
database:
postgres:
password: "super-secret-123" # DON'T DO THIS
# OK CORRECT: Use environment variable (managed by DevOps)
# export POSTGRES_PASSWORD=super-secret-123
# Or use K8s Secrets, Vault, etc.- Don't override everything in environment YAML
# [X] WRONG: Duplicating all defaults
infrastructure:
database:
postgres:
host: "prod-db.com"
port: 5432 # Redundant with default
user: "jan_user" # Redundant with default
database: "jan_llm_api" # Redundant with default
# OK CORRECT: Only override what's different
infrastructure:
database:
postgres:
host: "prod-db.com" # Only this is different- Don't rely on zero-value overrides
# [X] WRONG: Trying to "unset" a value
export AUTO_MIGRATE= # Empty string won't override true
# OK CORRECT: Explicitly set to false
export AUTO_MIGRATE=falseTesting Precedence
Unit Test Example
func TestConfigPrecedence(t *testing.T) {
// Set environment variable (priority 500)
os.Setenv("POSTGRES_PORT", "9999")
defer os.Clearenv()
// Create YAML file (priority 200)
yaml:= `
infrastructure:
database:
postgres:
port: 5433
`
os.WriteFile("config/defaults.yaml", []byte(yaml), 0644)
// Load configuration
loader:= config.NewConfigLoader("development", "config/defaults.yaml")
cfg, err:= loader.Load(context.Background())
// Environment variable should win
assert.Equal(t, 9999, cfg.Infrastructure.Database.Postgres.Port)
// Check provenance
prov:= loader.Provenance()
assert.Contains(t, prov, "env-vars") // Confirm env vars were loaded
}
## Troubleshooting
### Problem: Configuration not being applied
**Symptom:** Set `POSTGRES_PORT=9999` but service still uses 5432
**Solution:**
1. Check environment variable name matches `env` tag:
```go
Port int `env:"POSTGRES_PORT"` // Must be exact match- Verify environment variable is set in correct shell:
printenv | grep POSTGRES_PORT- Check provenance to see what overrode it:
info, _:= loader.Provenance("infrastructure.database.postgres.port")
fmt.Printf("Source: %s (priority %d)\n", info.Source, info.Priority)Problem: Can't override YAML value
Symptom: Changed production.yaml but using old value
Solution:
- Check file path matches environment:
ls config/environments/production.yaml # Must exist- Verify you're loading correct environment:
loader:= config.NewConfigLoader("production", "config/defaults.yaml")
// ^^^^^^^^^^^ Must match filename- Check for environment variable override (priority 500 > 300):
unset POSTGRES_HOST # Remove higher-priority overrideProblem: Defaults not updating
Symptom: Changed envDefault tag but defaults.yaml unchanged
Solution:
# Regenerate defaults.yaml
make config-generate
# Verify CI drift detection passes
go test -v./pkg/config -run TestConfigDriftSee Also
- Configuration README - Implementation details
- Config Types Reference - All configuration fields
- Code Generation - Schema and YAML generators
- Loader Tests - Precedence test examples