Runtime Configuration
Type-safe runtime configuration with watch callbacks and override resolution.
The config package provides runtime configuration management with type-safe accessors, automatic versioning, watch callbacks, and optional tenant-aware override resolution. Configuration entries are stored with full version history and can be observed for changes in real time.
Architecture
config.Service
├── config.Store (persistence with auto-versioning)
├── ValueResolver (optional: tenant override resolution)
└── WatchCallbacks (per-key change notifications)When a ValueResolver is attached (typically an override.Resolver), the type-safe accessors automatically resolve tenant-specific overrides from the context. Without a resolver, values are read directly from the config store.
Installation
import "github.com/xraph/vault/config"Creating a Service
svc := config.NewService(store,
config.WithAppID("myapp"),
config.WithResolver(resolver), // optional: enables tenant overrides
)ServiceOption
| Option | Signature | Description |
|---|---|---|
WithAppID | WithAppID(appID string) ServiceOption | Sets the default app ID for the service |
WithResolver | WithResolver(r ValueResolver) ServiceOption | Attaches a ValueResolver for tenant-aware config resolution |
ValueResolver interface
The ValueResolver is the bridge between the config service and the override system. The override.Resolver type satisfies this interface.
type ValueResolver interface {
Resolve(ctx context.Context, key, appID string) (any, error)
Invalidate(key, appID string)
}When a resolver is set:
- Type-safe accessors (
String,Bool,Int, etc.) callResolveinstead of reading the store directly. SetandDeletecallInvalidateto bust the resolver cache for the affected key.
Type-safe accessors
Each accessor evaluates the config key and returns a typed value. On error or type mismatch, the provided defaultVal is returned. If a ValueResolver is configured, these accessors use the resolver (which checks tenant overrides first). Otherwise, they read directly from the store.
String
func (s *Service) String(ctx context.Context, key, defaultVal string) stringregion := svc.String(ctx, "cloud-region", "us-east-1")Bool
func (s *Service) Bool(ctx context.Context, key string, defaultVal bool) boolmaintenance := svc.Bool(ctx, "maintenance-mode", false)Int
func (s *Service) Int(ctx context.Context, key string, defaultVal int) intHandles int, float64 (from JSON), and int64 values.
maxConns := svc.Int(ctx, "db-max-connections", 25)Float
func (s *Service) Float(ctx context.Context, key string, defaultVal float64) float64Handles float64, int, and int64 values.
rateLimit := svc.Float(ctx, "api-rate-limit", 100.0)Duration
func (s *Service) Duration(ctx context.Context, key string, defaultVal time.Duration) time.DurationParses string values using time.ParseDuration (e.g., "5s", "1m30s", "2h"). Also accepts numeric values interpreted as nanoseconds.
timeout := svc.Duration(ctx, "request-timeout", 30*time.Second)JSON
func (s *Service) JSON(ctx context.Context, key string, target any) errorEvaluates the config key and unmarshals the value into target (must be a pointer). Supports []byte, string, and arbitrary values via marshal/unmarshal round-trip.
type SMTPConfig struct {
Host string `json:"host"`
Port int `json:"port"`
}
var smtp SMTPConfig
if err := svc.JSON(ctx, "smtp-config", &smtp); err != nil {
smtp = SMTPConfig{Host: "localhost", Port: 587}
}CRUD operations
Get
Retrieves a config entry by key.
func (s *Service) Get(ctx context.Context, key, appID string) (*Entry, error)entry, err := svc.Get(ctx, "cloud-region", "myapp")
if err != nil {
log.Fatal(err)
}
fmt.Printf("key=%s value=%v version=%d\n", entry.Key, entry.Value, entry.Version)Set
Creates or updates a config entry. The store auto-increments the version on update. After writing, Set invalidates the resolver cache and notifies all watchers for the key.
func (s *Service) Set(
ctx context.Context,
key string,
value any,
appID string,
opts ...SetOption,
) errorerr := svc.Set(ctx, "cloud-region", "eu-west-1", "myapp",
config.WithDescription("Primary cloud region"),
config.WithValueType("string"),
config.WithMetadata(map[string]string{"source": "admin-panel"}),
)SetOption
| Option | Signature | Description |
|---|---|---|
WithDescription | WithDescription(desc string) SetOption | Sets a human-readable description |
WithValueType | WithValueType(vt string) SetOption | Explicitly labels the value type (e.g., "string", "int", "json") |
WithMetadata | WithMetadata(m map[string]string) SetOption | Attaches arbitrary key-value metadata |
If WithValueType is not provided, the type is inferred automatically:
| Go type | Inferred label |
|---|---|
string | "string" |
bool | "bool" |
int, int64 | "int" |
float64 | "float" |
| Everything else | "json" |
Delete
Removes a config entry and all its versions. Invalidates the resolver cache for the key.
func (s *Service) Delete(ctx context.Context, key, appID string) errorList
Returns config entries for an app.
func (s *Service) List(ctx context.Context, appID string, opts ListOpts) ([]*Entry, error)entries, err := svc.List(ctx, "myapp", config.ListOpts{
Limit: 100,
Offset: 0,
})
for _, e := range entries {
fmt.Printf("%-30s = %v (v%d)\n", e.Key, e.Value, e.Version)
}Watch
Register a callback that fires whenever a config key is updated via Set. Watchers receive both the old and new value.
func (s *Service) Watch(key string, cb WatchCallback)type WatchCallback func(ctx context.Context, key string, oldValue, newValue any)svc.Watch("feature-limit", func(ctx context.Context, key string, oldVal, newVal any) {
log.Printf("config %s changed: %v -> %v", key, oldVal, newVal)
reloadFeatureLimits(newVal)
})Watchers are invoked synchronously after the store write completes. If multiple watchers are registered for the same key, they are called in registration order.
Entity types
Entry
type Entry struct {
vault.Entity
ID id.ID `json:"id"`
Key string `json:"key"`
Value any `json:"value"`
ValueType string `json:"value_type"`
Version int64 `json:"version"`
Description string `json:"description"`
AppID string `json:"app_id"`
Metadata map[string]string `json:"metadata,omitempty"`
}| Field | Type | Description |
|---|---|---|
ID | id.ID | Unique config entry identifier (TypeID) |
Key | string | Lookup key for the entry |
Value | any | The configuration value |
ValueType | string | Type label ("string", "bool", "int", "float", "json") |
Version | int64 | Auto-incremented version number |
Description | string | Human-readable description |
AppID | string | Application scope identifier |
Metadata | map[string]string | Arbitrary key-value metadata |
CreatedAt | time.Time | From embedded vault.Entity |
UpdatedAt | time.Time | From embedded vault.Entity |
EntryVersion
Historical version record for a config entry.
type EntryVersion struct {
ID id.ID `json:"id"`
ConfigKey string `json:"config_key"`
AppID string `json:"app_id"`
Version int64 `json:"version"`
Value any `json:"value"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
}ListOpts
type ListOpts struct {
Limit int
Offset int
AppID string
}Store interface
type Store interface {
GetConfig(ctx context.Context, key, appID string) (*Entry, error)
SetConfig(ctx context.Context, e *Entry) error
DeleteConfig(ctx context.Context, key, appID string) error
ListConfig(ctx context.Context, appID string, opts ListOpts) ([]*Entry, error)
GetConfigVersion(ctx context.Context, key, appID string, version int64) (*Entry, error)
ListConfigVersions(ctx context.Context, key, appID string) ([]*EntryVersion, error)
}The store is responsible for:
- Auto-incrementing the
Versionfield onSetConfigwhen the key already exists. - Preserving version history accessible via
GetConfigVersionandListConfigVersions.
Parsing helpers
The package includes utility functions for parsing string values:
func ParseInt(s string) (int, error)
func ParseFloat(s string) (float64, error)
func ParseBool(s string) (bool, error)These are useful when working with configuration sources that provide string-only values (e.g., environment variables).
Full example
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/xraph/vault/config"
"github.com/xraph/vault/override"
"github.com/xraph/vault/scope"
)
func main() {
ctx := context.Background()
// Create a resolver for tenant-aware overrides.
resolver := override.NewResolver(configStore, overrideStore,
override.WithCacheTTL(1*time.Minute),
)
// Create the config service.
svc := config.NewService(configStore,
config.WithAppID("myapp"),
config.WithResolver(resolver),
)
// Set a config value.
err := svc.Set(ctx, "max-upload-size", 10485760, "myapp",
config.WithDescription("Maximum upload size in bytes"),
config.WithValueType("int"),
)
if err != nil {
log.Fatal(err)
}
// Watch for changes.
svc.Watch("max-upload-size", func(ctx context.Context, key string, old, new any) {
fmt.Printf("Upload limit changed: %v -> %v\n", old, new)
})
// Read with type safety.
maxSize := svc.Int(ctx, "max-upload-size", 5242880)
fmt.Printf("Max upload size: %d bytes\n", maxSize)
// Read with tenant context (resolver checks overrides).
ctx = scope.WithTenantID(ctx, "tenant-enterprise")
maxSize = svc.Int(ctx, "max-upload-size", 5242880)
fmt.Printf("Enterprise upload size: %d bytes\n", maxSize)
// Read a duration config.
timeout := svc.Duration(ctx, "request-timeout", 30*time.Second)
fmt.Printf("Timeout: %s\n", timeout)
}