Back to CourseLesson 2 of 10

Environment Configuration

Misconfigured environment variables are one of the most common causes of production incidents. This lesson covers .env management, runtime validation with Zod, secret rotation strategies, and the CoFounder project configuration file.

Structuring .env Files

Next.js loads .env.local automatically, but you should maintain a clear hierarchy of files. Never commit secrets to version control -- use.env.example as a template that documents every required variable without exposing real values.

# .env.example — committed to git, no real secrets
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
REDIS_URL=redis://localhost:6379
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
SENTRY_DSN=https://...@sentry.io/...
COFOUNDER_BUDGET_MONTHLY_USD=500

# .env.local — local overrides, gitignored
# .env.staging — loaded in staging CI/CD
# .env.production — loaded in production CI/CD (or use platform secrets)

Validating Environment Variables with Zod

Catching a missing or malformed variable at build time is far better than discovering it at 2 AM when your agent starts throwing cryptic errors. Use Zod to validate your entire environment at application startup:

// lib/env.ts
import { z } from 'zod';

const envSchema = z.object({
  NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
  NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(20),
  SUPABASE_SERVICE_ROLE_KEY: z.string().min(20),
  REDIS_URL: z.string().url(),
  OPENAI_API_KEY: z.string().startsWith('sk-'),
  SENTRY_DSN: z.string().url().optional(),
  NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'),
  COFOUNDER_BUDGET_MONTHLY_USD: z.coerce.number().positive().default(100),
});

export type Env = z.infer<typeof envSchema>;

function validateEnv(): Env {
  const result = envSchema.safeParse(process.env);
  if (!result.success) {
    console.error('Invalid environment variables:', result.error.flatten().fieldErrors);
    throw new Error('Environment validation failed. Check server logs.');
  }
  return result.data;
}

export const env = validateEnv();

Secret Rotation

API keys and database credentials should be rotated regularly. For LLM provider keys, CoFounder recommends a dual-key approach: generate a new key, update the environment variable, verify the new key works, then revoke the old key. Automate this with a rotation script:

// scripts/rotate-secrets.ts
import { execSync } from 'child_process';

const PLATFORM = process.env.DEPLOY_PLATFORM; // 'vercel' | 'aws'

async function rotateKey(name: string, newValue: string) {
  if (PLATFORM === 'vercel') {
    // Remove old, set new
    execSync(`vercel env rm ${name} production -y`);
    execSync(`echo "${newValue}" | vercel env add ${name} production`);
    console.log(`Rotated ${name} on Vercel`);
  } else if (PLATFORM === 'aws') {
    execSync(
      `aws ssm put-parameter --name "/myapp/prod/${name}" --value "${newValue}" --overwrite`
    );
    console.log(`Rotated ${name} in AWS SSM`);
  }
}

// Usage: npx tsx scripts/rotate-secrets.ts
rotateKey('OPENAI_API_KEY', process.argv[2]);

Per-Environment Configuration

Different environments need different settings beyond just credentials. Development might use verbose logging and mock LLM responses, staging mirrors production settings with lower rate limits, and production is fully locked down:

// lib/config.ts
import { env } from './env';

const configs = {
  development: {
    logLevel: 'debug' as const,
    llmTimeout: 30_000,
    maxAgentSteps: 50,
    enableMockLLM: true,
    rateLimitRPM: 1000,
  },
  staging: {
    logLevel: 'info' as const,
    llmTimeout: 15_000,
    maxAgentSteps: 20,
    enableMockLLM: false,
    rateLimitRPM: 120,
  },
  production: {
    logLevel: 'warn' as const,
    llmTimeout: 10_000,
    maxAgentSteps: 15,
    enableMockLLM: false,
    rateLimitRPM: 60,
  },
};

export const appConfig = configs[env.NODE_ENV];

CoFounder Project Configuration

The .cofounder.yml file lives at your project root and configures CoFounder CLI behavior, CI scanner rules, and deployment targets. It is the single source of truth for project-level settings:

# .cofounder.yml
project:
  name: my-ai-saas
  framework: nextjs

agents:
  default_provider: anthropic
  max_steps: 15
  timeout_ms: 10000

security:
  scanner:
    rules:
      - no-hardcoded-keys
      - no-exposed-assets
      - require-rls
  guards:
    input_sanitization: true
    output_filtering: true

cost:
  monthly_budget_usd: 500
  per_user_limit_usd: 10
  alert_threshold_percent: 80

deploy:
  platform: vercel
  region: us-east-1
  preview_branches: true