All articles
Deployment9 min readJanuary 14, 2026
Environment VariablesSecretsConfigurationBest Practices

Environment Variables for AI-Built Apps: A Complete Security Guide

The definitive guide to managing secrets across development, preview, and production environments for vibe coders.

Security Guide

Environment Variables: Your First Line of Defense

Environment variables keep secrets out of code. For AI-generated applications, they're essential—because AI frequently hardcodes credentials.

What Are Environment Variables?

Environment variables are configuration values stored outside your code:

bash
# Instead of this (in code)
const apiKey = "sk_live_abc123"

# You do this (in environment) const apiKey = process.env.API_KEY

The actual value lives in your deployment environment, not your repository.

The .env File System

File Hierarchy

.env                 # Shared defaults (committed, no secrets)
.env.local           # Local overrides (never committed)
.env.development     # Development defaults
.env.production      # Production defaults (no secrets)
.env.development.local  # Local dev secrets (never committed)
.env.production.local   # Local prod secrets (never committed)

Load Order (Next.js)

  1. .env.local (highest priority)
  2. .env.[environment].local
  3. .env.[environment]
  4. .env (lowest priority)

What Goes Where

.env (committed)

bash
# Non-sensitive defaults
NEXT_PUBLIC_APP_NAME=MyApp
NEXT_PUBLIC_API_URL=https://api.myapp.com
LOG_LEVEL=info

.env.local (never committed)

bash
# Actual secrets for local development
DATABASE_URL=postgres://localhost:5432/myapp
STRIPE_SECRET_KEY=sk_test_abc123
OPENAI_API_KEY=sk-proj-xyz789

.gitignore Configuration

Always include:

gitignore
# Environment files with secrets
.env.local
.env.*.local
.env.development.local
.env.production.local

# Common secret file patterns *.pem *.key credentials.json service-account.json

Platform-Specific Setup

Vercel

  1. Project Settings → Environment Variables
  2. Add variable name and value
  3. Select environments (Production, Preview, Development)
  4. Mark as "Sensitive" for secrets

Railway

  1. Project → Variables
  2. Add variable
  3. Automatically available to all deployments

Render

  1. Service → Environment
  2. Add Environment Variable
  3. Mark as secret if needed

GitHub Actions

  1. Repository → Settings → Secrets
  2. Add repository secret
  3. Access via secrets.SECRET_NAME
yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      DATABASE_URL: ${{ secrets.DATABASE_URL }}

Variable Naming Conventions

Server-Side Only

bash
# Use plain names - these stay on the server
DATABASE_URL=...
STRIPE_SECRET_KEY=...
JWT_SECRET=...

Client-Side Exposure

bash
# NEXT_PUBLIC_ prefix exposes to browser
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=...

Naming Best Practices

bash
# Good - Clear and descriptive
DATABASE_URL
STRIPE_SECRET_KEY
SUPABASE_SERVICE_ROLE_KEY

# Bad - Unclear DB KEY SECRET

Security Rules

Rule 1: Never Commit Secrets

bash
# Check for accidentally committed secrets
git log -p 
grep -i "api_key\secret\
password"

# If found, remove from history git filter-branch --force --index-filter \ 'git rm --cached --ignore-unmatch .env.local' \ --prune-empty --tag-name-filter cat -- --all

Rule 2: Rotate Compromised Secrets

If a secret was ever in your repo:

  1. Generate a new secret immediately
  2. Update all environments
  3. Invalidate the old secret
  4. Check for unauthorized usage

Rule 3: Different Secrets Per Environment

bash
# Production (Vercel Environment Variables)
STRIPE_SECRET_KEY=sk_live_real_key

# Development (.env.local) STRIPE_SECRET_KEY=sk_test_test_key

Rule 4: Minimal Exposure

javascript
// WRONG - Exposing secret to client
NEXT_PUBLIC_DATABASE_URL=postgres://user:pass@host/db

// RIGHT - Server only DATABASE_URL=postgres://user:pass@host/db

Common Environment Variables

Database

bash
DATABASE_URL=postgres://user:password@host:5432/database
MONGODB_URI=mongodb+srv://user:password@cluster/database

Authentication

bash
NEXTAUTH_SECRET=random-32-char-string
NEXTAUTH_URL=https://myapp.com
JWT_SECRET=another-random-string

Third-Party Services

bash
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
OPENAI_API_KEY=sk-...
RESEND_API_KEY=re_...

Supabase

bash
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...  # Safe for client
SUPABASE_SERVICE_ROLE_KEY=eyJhbGc...       # Server only!

Validation at Startup

Catch missing variables early:

javascript
// lib/env.ts
const requiredEnvVars = [
  'DATABASE_URL',
  'STRIPE_SECRET_KEY',
  'NEXTAUTH_SECRET',
]

for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { throw new Error(Missing required environment variable: \${envVar}) } }

export const env = { databaseUrl: process.env.DATABASE_URL!, stripeSecretKey: process.env.STRIPE_SECRET_KEY!, nextAuthSecret: process.env.NEXTAUTH_SECRET!, }

Type-Safe Environment Variables

With Zod:

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

const envSchema = z.object({ DATABASE_URL: z.string().url(), STRIPE_SECRET_KEY: z.string().startsWith('sk_'), NEXTAUTH_SECRET: z.string().min(32), NODE_ENV: z.enum(['development', 'production', 'test']), })

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

Environment Variable Checklist

SETUP
=====
[ ] .env.local in .gitignore
[ ] .env.*.local in .gitignore
[ ] No secrets in committed .env files
[ ] All secrets in platform environment variables

DEVELOPMENT =========== [ ] .env.local has all needed variables [ ] Using test keys (not production) [ ] Database points to dev environment

PRODUCTION ========== [ ] All secrets set in deployment platform [ ] Production keys are different from dev [ ] Sensitive variables marked as secret [ ] NEXTAUTH_URL set to production domain

MAINTENANCE =========== [ ] Secrets rotated regularly [ ] Unused variables removed [ ] Access to secrets limited to needed team members

The Bottom Line

Environment variables are simple but critical. Keep secrets out of code, never commit .env.local, and use different values for each environment.

One leaked secret can compromise everything. Environment variables prevent that leak.

Ready to secure your AI-generated code?

Stop reading about vulnerabilities. Start fixing them.

Start Scanning Free