zoobzio January 23, 2026 Edit this page

Secret Providers

This guide covers the SecretProvider interface, implementing custom providers, and best practices for secret management.

The SecretProvider Interface

type SecretProvider interface {
    Get(ctx context.Context, key string) (string, error)
}

Providers are simple: given a key, return a value. The context supports timeouts and cancellation for network-based providers.

Built-in Providers

fig includes providers for common secret stores:

PackageBackendKey Format
fig/vaultHashiCorp Vaultpath/to/secret:field
fig/awssmAWS Secrets Managersecret-name:json-field
fig/gcpsmGCP Secret Managersecret-name:json-field

See the integration docs for Vault, AWS Secrets Manager, and GCP Secret Manager for detailed documentation.

Using a Provider

Pass the provider to Load:

provider, err := vault.New()
if err != nil {
    log.Fatal(err)
}

var cfg Config
if err := fig.Load(&cfg, provider); err != nil {
    log.Fatal(err)
}

Only one provider is accepted. For multiple backends, see Composing Providers.

Implementing a Custom Provider

Basic Implementation

type EnvSecretProvider struct {
    prefix string
}

func NewEnvSecretProvider(prefix string) *EnvSecretProvider {
    return &EnvSecretProvider{prefix: prefix}
}

func (p *EnvSecretProvider) Get(ctx context.Context, key string) (string, error) {
    // Convert "db/password" to "SECRET_DB_PASSWORD"
    envKey := p.prefix + strings.ToUpper(strings.ReplaceAll(key, "/", "_"))

    val := os.Getenv(envKey)
    if val == "" {
        return "", fig.ErrSecretNotFound
    }
    return val, nil
}

With Caching

type CachingProvider struct {
    inner fig.SecretProvider
    cache map[string]string
    mu    sync.RWMutex
    ttl   time.Duration
}

func NewCachingProvider(inner fig.SecretProvider, ttl time.Duration) *CachingProvider {
    return &CachingProvider{
        inner: inner,
        cache: make(map[string]string),
        ttl:   ttl,
    }
}

func (p *CachingProvider) Get(ctx context.Context, key string) (string, error) {
    p.mu.RLock()
    if val, ok := p.cache[key]; ok {
        p.mu.RUnlock()
        return val, nil
    }
    p.mu.RUnlock()

    val, err := p.inner.Get(ctx, key)
    if err != nil {
        return "", err
    }

    p.mu.Lock()
    p.cache[key] = val
    p.mu.Unlock()

    return val, nil
}

With Retry Logic

type RetryProvider struct {
    inner      fig.SecretProvider
    maxRetries int
    backoff    time.Duration
}

func (p *RetryProvider) Get(ctx context.Context, key string) (string, error) {
    var lastErr error

    for i := 0; i <= p.maxRetries; i++ {
        val, err := p.inner.Get(ctx, key)
        if err == nil {
            return val, nil
        }

        // Don't retry for "not found"
        if err == fig.ErrSecretNotFound {
            return "", err
        }

        lastErr = err

        select {
        case <-ctx.Done():
            return "", ctx.Err()
        case <-time.After(p.backoff * time.Duration(i+1)):
        }
    }

    return "", fmt.Errorf("after %d retries: %w", p.maxRetries, lastErr)
}

Error Handling

ErrSecretNotFound

Return fig.ErrSecretNotFound when the secret doesn't exist:

func (p *MyProvider) Get(ctx context.Context, key string) (string, error) {
    val, err := p.client.Fetch(key)
    if errors.Is(err, client.ErrNotFound) {
        return "", fig.ErrSecretNotFound  // fig continues to env/default
    }
    if err != nil {
        return "", err  // fig fails with this error
    }
    return val, nil
}

Why this matters: ErrSecretNotFound tells fig to continue to the next source. Other errors cause Load to fail immediately.

Propagating Context Errors

Respect context cancellation:

func (p *MyProvider) Get(ctx context.Context, key string) (string, error) {
    select {
    case <-ctx.Done():
        return "", ctx.Err()
    default:
    }

    // ... fetch secret
}

Key Format Conventions

Path-Based Keys

Use forward slashes for hierarchical keys:

Password string `secret:"database/credentials/password"`
APIKey   string `secret:"services/stripe/api-key"`

Field Extraction

Use colons to extract fields from structured secrets:

// For JSON secrets like {"username": "admin", "password": "secret"}
Username string `secret:"database/credentials:username"`
Password string `secret:"database/credentials:password"`

Provider-Specific Prefixes

Some providers support prefixes to route to different backends:

// Custom provider that routes based on prefix
VaultSecret string `secret:"vault:db/password"`
AWSSecret   string `secret:"aws:api-key"`

Composing Providers

Multi-Backend Provider

Route to different providers based on key prefix:

type MultiProvider struct {
    providers map[string]fig.SecretProvider
    fallback  fig.SecretProvider
}

func (m *MultiProvider) Get(ctx context.Context, key string) (string, error) {
    prefix, actualKey, found := strings.Cut(key, ":")
    if found {
        if p, ok := m.providers[prefix]; ok {
            return p.Get(ctx, actualKey)
        }
    }

    if m.fallback != nil {
        return m.fallback.Get(ctx, key)
    }

    return "", fig.ErrSecretNotFound
}

Usage:

provider := &MultiProvider{
    providers: map[string]fig.SecretProvider{
        "vault": vaultProvider,
        "aws":   awsProvider,
    },
}

type Config struct {
    DBPassword  string `secret:"vault:database/password"`
    StripeKey   string `secret:"aws:stripe-api-key"`
}

Fallback Chain

Try multiple providers in order:

type ChainProvider struct {
    providers []fig.SecretProvider
}

func (c *ChainProvider) Get(ctx context.Context, key string) (string, error) {
    for _, p := range c.providers {
        val, err := p.Get(ctx, key)
        if err == nil {
            return val, nil
        }
        if err != fig.ErrSecretNotFound {
            return "", err  // Propagate real errors
        }
    }
    return "", fig.ErrSecretNotFound
}

Security Considerations

Logging

Never log secret values:

func (p *MyProvider) Get(ctx context.Context, key string) (string, error) {
    log.Printf("fetching secret: %s", key)  // OK: log key
    val, err := p.fetch(key)
    // log.Printf("got value: %s", val)      // BAD: never log values
    return val, err
}

Memory

Consider clearing secrets from memory when no longer needed:

type Config struct {
    Password string `secret:"db/password"`
}

func (c *Config) Clear() {
    c.Password = ""
}

Context Timeouts

Always use context for network operations:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := fig.LoadContext(ctx, &cfg, provider); err != nil {
    log.Fatal(err)
}

Next Steps