Configuration

Environment-based configuration with security-first design

goilerplate uses environment variables for configuration. All config lives in .env and is loaded into a typed struct.

Quick Start

Copy .env.example to .env:

cp .env.example .env

Edit the values:

# Application
APP_NAME=YourApp
APP_ENV=development                  # 'development' or 'production'
APP_URL=http://localhost:8090        # Base URL for emails & OAuth
APP_TAGLINE="Your awesome tagline"
[email protected]
PORT=8090

# Security (change these!)
JWT_SECRET=your-secret-key-change-this

# Token Expiry (Go duration format)
JWT_EXPIRY=168h                      # 7 days
TOKEN_EMAIL_VERIFY_EXPIRY=24h        # 24 hours
TOKEN_PASSWORD_RESET_EXPIRY=1h       # 1 hour
TOKEN_EMAIL_CHANGE_EXPIRY=24h        # 24 hours

# Database
# SQLite (default) - WAL mode enables concurrent readers
DB_DRIVER=sqlite
DB_CONNECTION=./data/app.db?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)

# PostgreSQL (optional)
#DB_DRIVER=pgx
#DB_CONNECTION=postgres://user:pass@localhost:5432/dbname?sslmode=disable

# OAuth (optional)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

# Email (resend.com)
[email protected]
RESEND_API_KEY=re_xxxxxxxxxxxxx
RESEND_AUDIENCE_ID=aud_xxxxxxxxxxxxx

# Payment Provider (polar or stripe)
PAYMENT_PROVIDER=polar

# Polar (polar.sh)
POLAR_API_KEY=polar_sk_xxxxxxxxxxxxx
POLAR_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx
POLAR_PRODUCT_ID_PRO_MONTHLY=prod_xxxxxxxxxxxxx
POLAR_PRODUCT_ID_PRO_YEARLY=prod_xxxxxxxxxxxxx

# Stripe (stripe.com) - Alternative
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx
STRIPE_PRICE_ID_PRO_MONTHLY=price_xxxxxxxxxxxxx
STRIPE_PRICE_ID_PRO_YEARLY=price_xxxxxxxxxxxxx

# Storage (S3-compatible) - Cloudflare R2 recommended (10GB free)
S3_REGION=auto
S3_BUCKET=my-app-dev
S3_ACCESS_KEY=your_r2_access_key
S3_SECRET_KEY=your_r2_secret_key
S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com  # Leave empty for AWS S3

# Analytics (optional)
UMAMI_WEBSITE_ID=
UMAMI_HOST=cloud.umami.is
PLAUSIBLE_DOMAIN=
PLAUSIBLE_HOST=plausible.io
GOOGLE_ANALYTICS_ID=

# Error Tracking (optional)
SENTRY_DSN=

# Logging (optional)
LOG_LEVEL=                           # debug | info | warn | error (empty = debug in dev, info in prod)
LOG_FORMAT=pretty                    # pretty (colored, for humans) | json (for log tools)
LOG_HTTP=true                        # HTTP request logging on/off

See Logging & Observability for the full breakdown of formats and switches.

The Sanitized Config Pattern

Problem: Templates need config values, but shouldn’t access secrets.

Solution: Sanitized() method creates a safe copy:

func (c *Config) Sanitized() *Config {
    return &Config{
        // Public values ✅
        AppName:      c.AppName,
        AppTagline:   c.AppTagline,
        SupportEmail: c.SupportEmail,

        // Secrets excluded ❌
        // JWTSecret: "",  // Empty
    }
}

How It Works

  1. Middleware adds sanitized config to context
  2. Templates get safe values only
  3. Services get full config via dependency injection
// In templates - safe
{{ cfg := ctxkeys.GetConfig(ctx) }}
<h1>{ cfg.AppName }</h1>     // ✅ Works
<p>{ cfg.JWTSecret }</p>     // ❌ Empty (secure!)

// In services - full access
authService := NewAuthService(cfg.JWTSecret)  // ✅ Full config

Whitelabeling

Zero hardcoded values = easy whitelabeling:

# Company A
APP_NAME="Acme Corp"
[email protected]

# Company B (same codebase!)
APP_NAME="TechStart"
[email protected]

Templates automatically use configured values in:

  • Logo text
  • Page titles
  • Email links
  • SEO metadata
  • Footer

Environment Detection

Use APP_ENV to handle dev/prod differences:

if cfg.IsProduction() {
    // Production settings
    secureOnly = true
} else {
    // Development settings
    debug = true
}

Security

What’s in Context (Public)

  • ✅ App name, tagline, URLs
  • ✅ Support email
  • ✅ Feature flags

What’s NOT in Context (Private)

  • ❌ JWT secrets
  • ❌ Database credentials
  • ❌ API keys

Best Practices

Never log full config:

// ❌ Bad - might expose secrets
log.Printf("Config: %+v", cfg)

// ✅ Good - log specific values
log.Printf("Starting %s on port %s", cfg.AppName, cfg.Port)

Validate required values:

JWTSecret: envRequired("JWT_SECRET"),  // No default!
AppName:   envString("APP_NAME", "Acme"),  // OK default

Never commit .env:

  • Use .env.example for documentation
  • Rotate secrets regularly
  • Use different secrets per environment