# Table of Contents

1. [Overview](#overview)
2. [Configuration Strategy](#configuration-strategy)
3. [Local Development (.env)](#local-development-env)
4. [OpenBao v2 Integration](#openbao-v2-integration)
5. [Config Loader Implementation](#config-loader-implementation)
6. [Provider Pattern Wiring](#provider-pattern-wiring)
7. [Environment Variables Reference](#environment-variables-reference)

---

## Overview

Configuration management following 12-Factor methodology:
- **Local dev:** `.env` file via golobby/dotenv
- **Staging/Prod:** OpenBao v2 KV secrets
- **No hardcoded values** in code

---

## Configuration Strategy

```go
func Load(ctx context.Context) (*Config, error) {
    env := os.Getenv("APP_ENV")
    if env == "" {
        env = "local"
    }

    switch env {
    case "local":
        return loadFromEnvFile()
    default:
        return loadFromOpenBao(ctx)
    }
}
```

| Environment | Config Source | Fallback |
|-------------|--------------|----------|
| `local` | `.env` file | - |
| `staging` | OpenBao | - |
| `production` | OpenBao | - |

---

## Local Development (.env)

**`.env` file:**

```env
APP_ENV=local
APP_MODE=debug
APP_PORT=8080
APP_ALLOWED_ORIGINS=http://localhost:3000

PERSISTENCE_DSN=postgres://user:pass@localhost:5432/messaging?sslmode=disable
CACHE_DSN=postgres://user:pass@localhost:5432/messaging?sslmode=disable&search_path=cache

KEYCLOAK_JWKS_URL=https://auth.pharos.id/realms/production/protocol/openid-connect/certs
KEYCLOAK_CLIENT_ID=exodus

LOG_LEVEL=debug
LOG_FORMAT=json
```

**Loading:**

```go
import "github.com/golobby/dotenv"

func loadFromEnvFile() (*Config, error) {
    if err := dotenv.Load(); err != nil {
        // .env optional for local dev
        if !os.IsNotExist(err) {
            return nil, err
        }
    }
    return loadFromEnv()
}
```

---

## OpenBao v2 Integration

**OpenBao Config:**

```go
type OpenBaoConfig struct {
    Addr       string `env:"OPENBAO_ADDR" required:"true"`
    Token      string `env:"OPENBAO_TOKEN" required:"true"`
    MountPath  string `env:"OPENBAO_MOUNT_PATH" default:"secret"`
    SecretPath string `env:"OPENBAO_SECRET_PATH" required:"true"`
}
```

**Secret Path Structure:**

```
secret/
└── data/
    └── backend/
        ├── local     # Local secrets
        ├── staging   # Staging secrets
        └── production # Production secrets
```

**Secret Content (KV v2):**

```json
{
  "data": {
    "PERSISTENCE_DSN": "postgres://...",
    "KEYCLOAK_JWKS_URL": "https://...",
    "KEYCLOAK_CLIENT_ID": "exodus"
  }
}
```

---

## Config Loader Implementation

```go
package config

import (
    "context"
    "os"

    "github.com/golobby/dotenv"
    "github.com/golobby/env/v2"
    "github.com/hashicorp/vault/v2/api"
)

func Load(ctx context.Context) (*Config, error) {
    env := os.Getenv("APP_ENV")
    if env == "" {
        env = "local"
    }

    if env == "local" {
        dotenv.Load()
        return loadFromEnv()
    }

    return loadFromOpenBao(ctx)
}

func loadFromEnv() (*Config, error) {
    var cfg Config
    if err := env.Parse(&cfg); err != nil {
        return nil, err
    }
    return &cfg, nil
}

func loadFromOpenBao(ctx context.Context) (*Config, error) {
    var vaultCfg OpenBaoConfig
    if err := env.Parse(&vaultCfg); err != nil {
        return nil, err
    }

    client, err := api.NewClient(&api.Config{
        Address: vaultCfg.Addr,
    })
    if err != nil {
        return nil, err
    }
    client.SetToken(vaultCfg.Token)

    // Path: {mount_path}/data/{secret_path}
    secret, err := client.KVv2(vaultCfg.MountPath).Get(ctx, vaultCfg.SecretPath)
    if err != nil {
        return nil, err
    }

    data := secret.Data["data"].(map[string]interface{})

    // Parse base config from env
    var cfg Config
    if err := env.Parse(&cfg); err != nil {
        return nil, err
    }

    // Apply secrets from OpenBao
    if dsn, ok := data["PERSISTENCE_DSN"].(string); ok {
        cfg.Persistence.DSN = dsn
    }
    if jwksURL, ok := data["KEYCLOAK_JWKS_URL"].(string); ok {
        cfg.Keycloak.JWKSURL = jwksURL
    }
    if clientID, ok := data["KEYCLOAK_CLIENT_ID"].(string); ok {
        cfg.Keycloak.ClientID = clientID
    }

    return &cfg, nil
}
```

---

## Provider Pattern Wiring

Explicit dependency injection via Provider struct.

```go
type Provider struct {
    Config          *Config
    Logger         *logger.Logger
    Metrics        *metrics.Prometheus
    Tracer         *tracing.TracerProvider
    PersistencePool *persistence.Pool
    CachePool      *persistence.Pool
    Keycloak       *keycloak.Provider
    UserRepo       outbound.UserRepository
    CacheRepo      outbound.CacheRepository
    AuthSvc        inbound.AuthService
    UserSvc        inbound.UserService
}

func NewProvider(ctx context.Context, cfg *Config) (*Provider, error) {
    p := &Provider{Config: cfg}

    if err := p.initLogger(); err != nil {
        return nil, err
    }
    if err := p.initMetrics(); err != nil {
        return nil, err
    }
    if err := p.initPersistence(ctx); err != nil {
        return nil, err
    }
    if err := p.initKeycloak(ctx); err != nil {
        return nil, err
    }

    p.UserRepo = persistence.NewUserRepository(p.PersistencePool)
    p.CacheRepo = cache.NewPostgresCacheRepository(p.CachePool)
    p.AuthSvc = usecase.NewAuthService(p.Keycloak, p.UserRepo)

    return p, nil
}
```

---

## Environment Variables Reference

| Variable | Description | Default |
|----------|-------------|---------|
| `APP_ENV` | Environment (local/staging/production) | `local` |
| `APP_MODE` | Gin mode (debug/release) | `release` |
| `APP_PORT` | HTTP server port | `8080` |
| `APP_ALLOWED_ORIGINS` | CORS origins | `*` |
| `PERSISTENCE_DSN` | PostgreSQL connection string | Required |
| `PERSISTENCE_MAX_CONNS` | Max DB connections | `25` |
| `PERSISTENCE_MIN_CONNS` | Min DB connections | `5` |
| `CACHE_DSN` | Cache DB connection string | Required |
| `KEYCLOAK_JWKS_URL` | Keycloak JWKS endpoint | Required |
| `KEYCLOAK_CLIENT_ID` | Keycloak client ID | Required |
| `LOG_LEVEL` | Logging level | `info` |
| `LOG_FORMAT` | Log format (json/text) | `json` |
| `TEMPO_ENABLED` | Enable tracing | `false` |
| `TEMPO_ENDPOINT` | Tempo OTLP endpoint | `tempo:4318` |
| `OPENBAO_ADDR` | OpenBao server address | Required (non-local) |
| `OPENBAO_TOKEN` | OpenBao token | Required (non-local) |
| `OPENBAO_MOUNT_PATH` | KV v2 mount path | `secret` |
| `OPENBAO_SECRET_PATH` | Secret path | Required (non-local) |
