---
title: Forge Integration
description: Using Vault with the Forge framework.
---

This guide covers patterns for integrating Vault into a [Forge](https://github.com/xraph/forge)-style application. While Vault does not ship a pre-built Forge extension, the patterns below show how to wire stores, services, middleware, and lifecycle management into Forge's architecture.

## HTTP middleware for scope injection

Vault uses context-based scoping via the `scope` package. In an HTTP application, extract the tenant ID, user ID, and other identifiers from the request (JWT claims, headers, API keys) and inject them into the context before handlers execute.

```go
package middleware

import (
    "net/http"

    "github.com/xraph/vault/scope"
)

// VaultScope extracts tenant and user information from the request
// and injects it into the context for Vault operations.
func VaultScope(appID string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx := r.Context()

            // Set the application ID.
            ctx = scope.WithAppID(ctx, appID)

            // Extract tenant ID from header or JWT claims.
            if tenantID := r.Header.Get("X-Tenant-ID"); tenantID != "" {
                ctx = scope.WithTenantID(ctx, tenantID)
            }

            // Extract user ID from JWT claims or auth header.
            if userID := r.Header.Get("X-User-ID"); userID != "" {
                ctx = scope.WithUserID(ctx, userID)
            }

            // Capture client IP for audit logging.
            ctx = scope.WithIP(ctx, r.RemoteAddr)

            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}
```

This middleware ensures that every downstream Vault operation (secret access, flag evaluation, config reads, audit logging) has the correct scope context. The `scope` package keys intentionally match the context keys used by `flag.Engine` and `override.Resolver`, so values are automatically visible across all subsystems.

## Dependency registration pattern

In a Forge-style application with a dependency injection container, register the store and all services at startup:

```go
package main

import (
    "context"
    "log"
    "log/slog"

    "github.com/xraph/vault/audit"
    audithook "github.com/xraph/vault/audit_hook"
    "github.com/xraph/vault/config"
    "github.com/xraph/vault/crypto"
    "github.com/xraph/vault/flag"
    "github.com/xraph/vault/override"
    "github.com/xraph/vault/rotation"
    "github.com/xraph/vault/secret"
    "github.com/xraph/vault/store/postgres"
)

// VaultServices holds all Vault service instances.
type VaultServices struct {
    Store      *postgres.Store
    Secrets    *secret.Service
    Flags      *flag.Service
    FlagEngine *flag.Engine
    Config     *config.Service
    Resolver   *override.Resolver
    Rotation   *rotation.Manager
    Audit      *audit.Logger
}

// NewVaultServices wires up all Vault services.
func NewVaultServices(ctx context.Context, connString string, encKey []byte, appID string) (*VaultServices, error) {
    // Create the store.
    store, err := postgres.New(ctx, connString,
        postgres.WithLogger(slog.Default()),
    )
    if err != nil {
        return nil, err
    }

    // Run migrations.
    if err := store.Migrate(ctx); err != nil {
        return nil, err
    }

    // Create the encryptor.
    enc, err := crypto.NewEncryptor(encKey)
    if err != nil {
        return nil, err
    }

    // Wire up services.
    secretSvc := secret.NewService(store, enc, secret.WithAppID(appID))
    resolver := override.NewResolver(store, store)
    cfgSvc := config.NewService(store,
        config.WithAppID(appID),
        config.WithResolver(resolver),
    )
    flagEngine := flag.NewEngine(store)
    flagSvc := flag.NewService(flagEngine, flag.WithAppID(appID))
    rotMgr := rotation.NewManager(store, secretSvc,
        rotation.WithAppID(appID),
    )

    // Create audit logger with an optional hook.
    hook := audithook.New(audithook.RecorderFunc(
        func(ctx context.Context, event *audithook.AuditEvent) error {
            slog.Info("audit",
                "action", event.Action,
                "resource", event.Resource,
                "key", event.Key,
                "outcome", event.Outcome,
            )
            return nil
        },
    ))
    auditLogger := audit.NewLogger(store, audit.WithHook(hook))

    return &VaultServices{
        Store:      store,
        Secrets:    secretSvc,
        Flags:      flagSvc,
        FlagEngine: flagEngine,
        Config:     cfgSvc,
        Resolver:   resolver,
        Rotation:   rotMgr,
        Audit:      auditLogger,
    }, nil
}
```

In a Forge DI container, you would register each service as a singleton:

```go
func RegisterVault(container *di.Container) {
    // Register the store.
    di.Singleton(container, func() (*postgres.Store, error) {
        return postgres.New(ctx, os.Getenv("DATABASE_URL"))
    })

    // Register the secret service.
    di.Singleton(container, func(store *postgres.Store, enc *crypto.Encryptor) *secret.Service {
        return secret.NewService(store, enc, secret.WithAppID("myapp"))
    })

    // Register the flag engine and service.
    di.Singleton(container, func(store *postgres.Store) *flag.Engine {
        return flag.NewEngine(store)
    })
    di.Singleton(container, func(engine *flag.Engine) *flag.Service {
        return flag.NewService(engine, flag.WithAppID("myapp"))
    })

    // Register the config service with override resolver.
    di.Singleton(container, func(store *postgres.Store) *override.Resolver {
        return override.NewResolver(store, store)
    })
    di.Singleton(container, func(store *postgres.Store, resolver *override.Resolver) *config.Service {
        return config.NewService(store,
            config.WithAppID("myapp"),
            config.WithResolver(resolver),
        )
    })
}
```

## Lifecycle management

The rotation manager runs a background loop that checks for due rotations. Start it during application boot and stop it during graceful shutdown.

```go
func main() {
    ctx := context.Background()

    svc, err := NewVaultServices(ctx, connString, encKey, "myapp")
    if err != nil {
        log.Fatal(err)
    }

    // Register rotator functions.
    svc.Rotation.RegisterRotator("database_url", func(ctx context.Context, current []byte) ([]byte, error) {
        // Call your password rotation API here.
        newPass := generatePassword()
        return []byte(fmt.Sprintf("postgres://app:%s@db.internal:5432/prod", newPass)), nil
    })

    // Start the rotation manager background loop.
    if err := svc.Rotation.Start(ctx); err != nil {
        log.Fatal("start rotation manager:", err)
    }

    // Start your HTTP server...
    server := &http.Server{Addr: ":8080", Handler: router}
    go server.ListenAndServe()

    // Wait for shutdown signal.
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    // Graceful shutdown.
    shutdownCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()

    // Stop the rotation manager first.
    if err := svc.Rotation.Stop(shutdownCtx); err != nil {
        log.Printf("rotation stop: %v", err)
    }

    // Shut down HTTP server.
    server.Shutdown(shutdownCtx)

    // Close the store last.
    svc.Store.Close()
}
```

## Using Vault in HTTP handlers

With the scope middleware and DI-registered services, HTTP handlers can use Vault naturally:

```go
func GetSecretHandler(secretSvc *secret.Service, auditLog *audit.Logger) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        key := r.URL.Query().Get("key")

        sec, err := secretSvc.Get(ctx, key, "")
        if err != nil {
            auditLog.LogFailure(ctx, key, "secret.accessed", "secret", err)
            http.Error(w, "secret not found", http.StatusNotFound)
            return
        }

        auditLog.LogAccess(ctx, key, "secret.accessed", "secret")
        w.Write(sec.Value)
    }
}

func GetConfigHandler(cfgSvc *config.Service) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        // The resolver automatically checks for tenant overrides
        // because the scope middleware injected the tenant ID.
        limit := cfgSvc.Int(ctx, "rate_limit", 100)
        fmt.Fprintf(w, `{"rate_limit": %d}`, limit)
    }
}

func FeatureFlagHandler(flagSvc *flag.Service) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        // Flag evaluation uses the tenant ID from context
        // for tenant overrides and targeting rules.
        enabled := flagSvc.Bool(ctx, "new_dashboard", false)
        fmt.Fprintf(w, `{"new_dashboard": %v}`, enabled)
    }
}
```

## Wiring the router

```go
func setupRouter(svc *VaultServices) http.Handler {
    mux := http.NewServeMux()

    mux.HandleFunc("GET /api/secrets", GetSecretHandler(svc.Secrets, svc.Audit))
    mux.HandleFunc("GET /api/config", GetConfigHandler(svc.Config))
    mux.HandleFunc("GET /api/flags", FeatureFlagHandler(svc.Flags))

    // Wrap with scope middleware.
    return middleware.VaultScope("myapp")(mux)
}
```

## Grove database integration

When your Forge app uses the [Grove extension](https://github.com/xraph/grove) to manage database connections, Vault can automatically resolve a `grove.DB` from the DI container and construct the correct store backend based on the driver type.

### Using the default grove database

If the Grove extension registers a single database (or a default in multi-DB mode), use `WithGroveDatabase` with an empty name:

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

ext := extension.New(
    extension.WithGroveDatabase(""),
)
```

### Using a named grove database

In multi-database setups, reference a specific database by name:

```go
ext := extension.New(
    extension.WithGroveDatabase("vault"),
)
```

This resolves the grove.DB named `"vault"` from the DI container and auto-constructs the matching store. The driver type is detected automatically -- you do not need to import individual store packages.

### Store resolution order

The extension resolves its store in this order:

1. **Explicit store** -- if an explicit store was provided, it is used directly and grove is ignored.
2. **Grove database** -- if `WithGroveDatabase(name)` was called, the named or default `grove.DB` is resolved from DI.
3. **In-memory fallback** -- if neither is configured, an in-memory store is used.

## Config change watchers

The config service supports watchers that fire when a config entry is updated. Use this to react to configuration changes at runtime:

```go
cfgSvc.Watch("rate_limit", func(ctx context.Context, key string, oldValue, newValue any) {
    slog.Info("config changed",
        "key", key,
        "old", oldValue,
        "new", newValue,
    )
    // Update your rate limiter, reconfigure a connection pool, etc.
})
```

## Mounting vault into confy

The vault extension can automatically register itself as a confy `ConfigSource` and `SecretProvider`, making vault-managed config and secrets accessible through forge's `ConfigManager`.

### Basic mount

```yaml
extensions:
  vault:
    app_id: myapp
    mount_to_confy: true
```

Or programmatically:

```go
ext := extension.New(
    extension.WithAppID("myapp"),
    extension.WithMountToConfy(),
)
```

### Selective mounting

Control which vault keys are exposed to confy using key prefixes, whitelists, and glob patterns:

```yaml
extensions:
  vault:
    app_id: myapp
    mount_to_confy: true
    confy_key_prefix: "vault."
    confy_mount_keys:
      - db.host
      - api_key
    confy_mount_patterns:
      - "auth.*"
      - "feature.*"
```

After mounting, vault config is accessible through the standard confy API:

```go
cm := app.Config()
dbHost := cm.GetString("vault.db.host", "localhost")
```

For detailed documentation, see the [Confy Integration](/docs/vault/guides/confy-integration) guide.
