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.sharedas single source of truth. Runpnpm env:generateto 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.sharedin any environment, always runpnpm 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
- Environment Management: Automated environment system
- Environment Variables: Complete variable reference
- Docker: Docker configuration
- Docker Compose: Multi-service setup
- Production: Production deployment
Ready to deploy? Continue with Production Deployment →