Docs /deployment/environment-setup

Environment Setup

Production environment configuration and secrets management

📋 Overview

Proper environment configuration is critical for security and functionality. This guide covers environment variables, secrets management, and configuration strategies for production deployments.

🔐 Environment Variables

Required Variables

Database Configuration

# PostgreSQL connection string
DATABASE_URL="postgresql://username:password@host:5432/database"

# Alternative: separate components
DB_HOST="postgres.example.com"
DB_PORT="5432"
DB_NAME="izri"
DB_USER="app_user"
DB_PASSWORD="secure_password"

Authentication

# Better Auth
BETTER_AUTH_SECRET="random_32_char_secret_key_here"
BETTER_AUTH_URL="https://api.yourdomain.com"

# GitHub OAuth
GITHUB_CLIENT_ID="Iv1.1234567890abcdef"
GITHUB_CLIENT_SECRET="1234567890abcdef1234567890abcdef12345678"
GITHUB_REDIRECT_URI="https://yourdomain.com/auth/callback/github"

# JWT for API tokens
JWT_SECRET="another_secure_random_secret_key"
JWT_EXPIRES_IN="7d"

AI Services

# OpenAI
OPENAI_API_KEY="sk-proj-..."
OPENAI_MODEL="gpt-4"
OPENAI_MAX_TOKENS="2000"

# Anthropic (optional)
ANTHROPIC_API_KEY="sk-ant-..."
ANTHROPIC_MODEL="claude-3-opus-20240229"

Redis

# Redis connection
REDIS_URL="redis://redis.example.com:6379"

# Alternative: separate components
REDIS_HOST="redis.example.com"
REDIS_PORT="6379"
REDIS_PASSWORD="redis_password"
REDIS_DB="0"

Application Settings

# Environment
NODE_ENV="production"

# API Server
API_PORT="4000"
API_HOST="0.0.0.0"
LOG_LEVEL="info"

# Web Application
VITE_API_URL="https://api.yourdomain.com"
PORT="3000"

Optional Variables

Observability

# OpenTelemetry
OTEL_EXPORTER_OTLP_ENDPOINT="https://otel-collector:4318"
OTEL_SERVICE_NAME="izri-api"
OTEL_LOG_LEVEL="info"

# Sentry
SENTRY_DSN="https://...@sentry.io/..."
SENTRY_ENVIRONMENT="production"
SENTRY_TRACES_SAMPLE_RATE="0.1"

External Services

# Email (if implemented)
SMTP_HOST="smtp.sendgrid.net"
SMTP_PORT="587"
SMTP_USER="apikey"
SMTP_PASSWORD="SG...."
SMTP_FROM="noreply@yourdomain.com"

# S3 (if implemented)
AWS_REGION="us-east-1"
AWS_ACCESS_KEY_ID="AKIA..."
AWS_SECRET_ACCESS_KEY="..."
S3_BUCKET="izri-uploads"

Rate Limiting

# API rate limits
RATE_LIMIT_WINDOW_MS="60000"
RATE_LIMIT_MAX_REQUESTS="100"

# AI service rate limits
AI_RATE_LIMIT_MAX_REQUESTS="10"
AI_RATE_LIMIT_WINDOW_MS="60000"

📝 Environment File Structure

Development (.env.development)

NODE_ENV=development
LOG_LEVEL=debug

# Local services
DATABASE_URL=postgresql://postgres:password@localhost:5433/izri_dev
REDIS_URL=redis://localhost:6380

# API
API_PORT=4000
VITE_API_URL=http://localhost:4000

# Auth (GitHub test app)
GITHUB_CLIENT_ID=dev_client_id
GITHUB_CLIENT_SECRET=dev_client_secret
BETTER_AUTH_SECRET=dev_secret_key_min_32_chars_long
BETTER_AUTH_URL=http://localhost:4000

# AI (test key with limits)
OPENAI_API_KEY=sk-...

Production (.env.production)

NODE_ENV=production
LOG_LEVEL=info

# Production database
DATABASE_URL=postgresql://prod_user:${DB_PASSWORD}@prod-db.region.rds.amazonaws.com:5432/izri

# Production Redis
REDIS_URL=redis://:${REDIS_PASSWORD}@prod-redis.cache.amazonaws.com:6379

# API
API_PORT=4000
API_HOST=0.0.0.0
VITE_API_URL=https://api.yourdomain.com

# Auth (production GitHub app)
GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
BETTER_AUTH_URL=https://api.yourdomain.com

# AI
OPENAI_API_KEY=${OPENAI_API_KEY}

# Monitoring
SENTRY_DSN=${SENTRY_DSN}
OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_ENDPOINT}

Staging (.env.staging)

NODE_ENV=staging
LOG_LEVEL=debug

# Staging database
DATABASE_URL=postgresql://staging_user:${DB_PASSWORD}@staging-db.example.com:5432/izri_staging

# Staging Redis
REDIS_URL=redis://staging-redis.example.com:6379

# API
API_PORT=4000
VITE_API_URL=https://staging-api.yourdomain.com

# Auth (staging GitHub app)
GITHUB_CLIENT_ID=${STAGING_GITHUB_CLIENT_ID}
GITHUB_CLIENT_SECRET=${STAGING_GITHUB_CLIENT_SECRET}
BETTER_AUTH_SECRET=${STAGING_AUTH_SECRET}
BETTER_AUTH_URL=https://staging-api.yourdomain.com

# AI (separate key for staging)
OPENAI_API_KEY=${STAGING_OPENAI_API_KEY}

🔒 Secrets Management

Option 1: AWS Secrets Manager

Store secrets:

# Create secret
aws secretsmanager create-secret \
  --name izri/production \
  --secret-string '{
    "DATABASE_URL": "postgresql://...",
    "GITHUB_CLIENT_SECRET": "...",
    "OPENAI_API_KEY": "sk-..."
  }'

Retrieve in application:

import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'

const client = new SecretsManagerClient({ region: 'us-east-1' })

async function getSecrets() {
  const response = await client.send(
    new GetSecretValueCommand({ SecretId: 'izri/production' })
  )
  return JSON.parse(response.SecretString!)
}

Docker integration:

# docker-compose.yml
services:
  api:
    environment:
      - AWS_REGION=us-east-1
      - SECRET_NAME=izri/production
    command: sh -c "
      export $(aws secretsmanager get-secret-value --secret-id $SECRET_NAME --query SecretString --output text | jq -r 'to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]') &&
      node apps/api/dist/index.js
    "

Option 2: HashiCorp Vault

Store secrets:

# Enable KV secrets engine
vault secrets enable -path=izri kv-v2

# Store secrets
vault kv put izri/production \
  database_url="postgresql://..." \
  github_client_secret="..." \
  openai_api_key="sk-..."

Retrieve in application:

import vault from 'node-vault'

const client = vault({
  endpoint: 'https://vault.example.com:8200',
  token: process.env.VAULT_TOKEN
})

async function getSecrets() {
  const result = await client.read('izri/production')
  return result.data.data
}

Option 3: Kubernetes Secrets

Create secret:

kubectl create secret generic izri-secrets \
  --from-literal=database-url='postgresql://...' \
  --from-literal=github-client-secret='...' \
  --from-literal=openai-api-key='sk-...'

Use in deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: izri-api
spec:
  template:
    spec:
      containers:
      - name: api
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: izri-secrets
              key: database-url
        - name: GITHUB_CLIENT_SECRET
          valueFrom:
            secretKeyRef:
              name: izri-secrets
              key: github-client-secret

Option 4: Doppler

Install CLI:

# Install
brew install dopplerhq/cli/doppler

# Login
doppler login

# Setup project
doppler setup

Use in development:

# Run with Doppler
doppler run -- pnpm dev

# Docker integration
docker run --env-file <(doppler secrets download --no-file --format docker) izri-api

Option 5: Environment Variables (Platform-specific)

Railway:

# Set via CLI
railway variables set DATABASE_URL="postgresql://..."

# Or via dashboard: project → Variables

Render:

# render.yaml
services:
  - type: web
    name: izri-api
    env: docker
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: postgres
          property: connectionString
      - key: GITHUB_CLIENT_SECRET
        sync: false  # Manual secret input

Fly.io:

# Set secrets
fly secrets set DATABASE_URL="postgresql://..." \
  GITHUB_CLIENT_SECRET="..." \
  OPENAI_API_KEY="sk-..."

# List secrets
fly secrets list

🔧 Configuration Validation

Runtime Validation

Create schema (src/config/env.ts):

import { z } from 'zod'

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'staging', 'production']),
  
  // Database
  DATABASE_URL: z.string().url(),
  
  // Redis
  REDIS_URL: z.string().url(),
  
  // Auth
  GITHUB_CLIENT_ID: z.string().min(1),
  GITHUB_CLIENT_SECRET: z.string().min(1),
  BETTER_AUTH_SECRET: z.string().min(32),
  BETTER_AUTH_URL: z.string().url(),
  
  // AI
  OPENAI_API_KEY: z.string().startsWith('sk-'),
  
  // API
  API_PORT: z.coerce.number().default(4000),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
  
  // Optional
  SENTRY_DSN: z.string().url().optional(),
  OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(),
})

export const env = envSchema.parse(process.env)

export type Env = z.infer<typeof envSchema>

Use validated config:

import { env } from './config/env'

// Type-safe and validated
const dbUrl = env.DATABASE_URL
const port = env.API_PORT  // number, not string

Startup Validation

// src/index.ts
import { env } from './config/env'
import { logger } from './lib/logger'

async function validateEnvironment() {
  logger.info('Validating environment configuration...')
  
  try {
    // Already validated by Zod, but can add runtime checks
    
    // Test database connection
    await db.select().from(users).limit(1)
    logger.info('✓ Database connection successful')
    
    // Test Redis connection
    await redis.ping()
    logger.info('✓ Redis connection successful')
    
    logger.info('✓ Environment validation complete')
  } catch (error) {
    logger.error('✗ Environment validation failed:', error)
    process.exit(1)
  }
}

await validateEnvironment()

🎯 Best Practices

1. Never Commit Secrets

Add to .gitignore:

# Environment files
.env
.env.*
.env.local
.env.*.local
.env.production
.env.shared  # Important: Never commit with real secrets!

# But keep examples and configs
!.env.shared.example
!env.config.ts

2. Use Example Files

Izri uses a centralized environment system. Keep .env.shared.example updated:

Maintain .env.shared.example:

# Database
DATABASE_URL="postgresql://user:password@localhost:5433/izri"

# Auth
GITHUB_CLIENT_ID="your_client_id"
GITHUB_CLIENT_SECRET="your_client_secret"
BETTER_AUTH_SECRET="generate_a_random_32_char_secret"

# AI
OPENAI_API_KEY="sk-your-key-here"

# Optional
SENTRY_DSN=""

💡 New System: We use .env.shared as single source of truth. Run pnpm env:generate to create project-specific files. See Environment Management.

3. Rotate Secrets Regularly

# Generate new secrets
openssl rand -base64 32

# Update in .env.shared
# Edit .env.shared with new secrets
nano .env.shared

# Regenerate project-specific files
pnpm env:generate

# Or in CI/CD, update secrets manager
aws secretsmanager update-secret \
  --secret-id izri/production \
  --secret-string '{"BETTER_AUTH_SECRET": "new_secret"}'

# Restart services
docker compose restart api

4. Use Different Secrets Per Environment

Development (.env.shared in local):

BETTER_AUTH_SECRET="dev_secret_32_chars_long_here_"

Staging (.env.shared from secrets manager):

BETTER_AUTH_SECRET="staging_secret_different_one"

Production (.env.shared from secrets manager):

BETTER_AUTH_SECRET="prod_secret_completely_unique"

After updating .env.shared in any environment, always run pnpm env:generate

5. Principle of Least Privilege

# Create dedicated database user with limited permissions
CREATE USER api_user WITH PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE izri TO api_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO api_user;

# Don't use superuser accounts

🔗 Related Documentation


Ready to deploy? Continue with Production Deployment

Reading this with an agent? /docs/deployment/environment-setup.md serves the raw markdown.

All docs