Vault

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

OptionSignatureDescription
WithAppIDWithAppID(appID string) ServiceOptionSets the default app ID for the service
WithResolverWithResolver(r ValueResolver) ServiceOptionAttaches 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.) call Resolve instead of reading the store directly.
  • Set and Delete call Invalidate to 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) string
region := svc.String(ctx, "cloud-region", "us-east-1")

Bool

func (s *Service) Bool(ctx context.Context, key string, defaultVal bool) bool
maintenance := svc.Bool(ctx, "maintenance-mode", false)

Int

func (s *Service) Int(ctx context.Context, key string, defaultVal int) int

Handles 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) float64

Handles 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.Duration

Parses 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) error

Evaluates 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,
) error
err := 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

OptionSignatureDescription
WithDescriptionWithDescription(desc string) SetOptionSets a human-readable description
WithValueTypeWithValueType(vt string) SetOptionExplicitly labels the value type (e.g., "string", "int", "json")
WithMetadataWithMetadata(m map[string]string) SetOptionAttaches arbitrary key-value metadata

If WithValueType is not provided, the type is inferred automatically:

Go typeInferred 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) error

List

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"`
}
FieldTypeDescription
IDid.IDUnique config entry identifier (TypeID)
KeystringLookup key for the entry
ValueanyThe configuration value
ValueTypestringType label ("string", "bool", "int", "float", "json")
Versionint64Auto-incremented version number
DescriptionstringHuman-readable description
AppIDstringApplication scope identifier
Metadatamap[string]stringArbitrary key-value metadata
CreatedAttime.TimeFrom embedded vault.Entity
UpdatedAttime.TimeFrom 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 Version field on SetConfig when the key already exists.
  • Preserving version history accessible via GetConfigVersion and ListConfigVersions.

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)
}

On this page