---
title: Runtime Configuration
description: 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

```go
import "github.com/xraph/vault/config"
```

## Creating a Service

```go
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.

```go
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

```go
func (s *Service) String(ctx context.Context, key, defaultVal string) string
```

```go
region := svc.String(ctx, "cloud-region", "us-east-1")
```

### Bool

```go
func (s *Service) Bool(ctx context.Context, key string, defaultVal bool) bool
```

```go
maintenance := svc.Bool(ctx, "maintenance-mode", false)
```

### Int

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

Handles `int`, `float64` (from JSON), and `int64` values.

```go
maxConns := svc.Int(ctx, "db-max-connections", 25)
```

### Float

```go
func (s *Service) Float(ctx context.Context, key string, defaultVal float64) float64
```

Handles `float64`, `int`, and `int64` values.

```go
rateLimit := svc.Float(ctx, "api-rate-limit", 100.0)
```

### Duration

```go
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.

```go
timeout := svc.Duration(ctx, "request-timeout", 30*time.Second)
```

### JSON

```go
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.

```go
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.

```go
func (s *Service) Get(ctx context.Context, key, appID string) (*Entry, error)
```

```go
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.

```go
func (s *Service) Set(
    ctx context.Context,
    key string,
    value any,
    appID string,
    opts ...SetOption,
) error
```

```go
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

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

```go
func (s *Service) Delete(ctx context.Context, key, appID string) error
```

### List

Returns config entries for an app.

```go
func (s *Service) List(ctx context.Context, appID string, opts ListOpts) ([]*Entry, error)
```

```go
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.

```go
func (s *Service) Watch(key string, cb WatchCallback)
```

```go
type WatchCallback func(ctx context.Context, key string, oldValue, newValue any)
```

```go
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

```go
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.

```go
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

```go
type ListOpts struct {
    Limit  int
    Offset int
    AppID  string
}
```

## Store interface

```go
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:

```go
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

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