Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
12 minLesson 38 of 40
Deployment & DevOps

Environment Variables & Secrets

Environment Variables and Secrets Management

Every application has configuration that changes between environments: database connections for dev vs. production, API keys that shouldn't be in version control, feature flags for staged rollouts. Environment variables are the standard solution — values injected into your app at runtime, not hardcoded.

Why This Matters

Hardcoded secrets are one of the most common security mistakes. Dozens of GitHub repositories have had database passwords and API keys committed accidentally. GitHub now scans for known secret patterns and alerts you — and attackers scan public repos too.

// ❌ Never do this
const client = new DatabaseClient({
    host: "db.myapp.com",
    password: "supersecret123",    // now in version control forever
});

// ✅ Use environment variables
const client = new DatabaseClient({
    host: process.env.DB_HOST,
    password: process.env.DB_PASSWORD,
});

.env Files

Create .env files for local development and always add them to .gitignore:

# .gitignore — these must be excluded from version control
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# .env.local — your local development values
DATABASE_URL=postgresql://postgres:password@localhost:5432/myapp_dev
JWT_SECRET=dev-secret-not-for-production
STRIPE_SECRET_KEY=sk_test_your_test_key_here
STRIPE_PUBLISHABLE_KEY=pk_test_your_test_key_here
RESEND_API_KEY=re_test_your_key_here
NEXT_PUBLIC_APP_URL=http://localhost:3000
# .env.example — committed to version control, shows what variables are needed
DATABASE_URL=
JWT_SECRET=
STRIPE_SECRET_KEY=
STRIPE_PUBLISHABLE_KEY=
RESEND_API_KEY=
NEXT_PUBLIC_APP_URL=

Commit .env.example with all the variable names but no values. New team members know exactly what to fill in.

Next.js Environment Variables

Next.js has a specific convention:

# Server-only (not sent to the browser)
DATABASE_URL=postgresql://...
JWT_SECRET=...
STRIPE_SECRET_KEY=sk_live_...

# Public (sent to the browser — anyone can see these)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_APP_URL=https://myapp.com
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=G-XXXXXXXX

The NEXT_PUBLIC_ prefix tells Next.js to include this variable in the client-side bundle. Without it, the variable is undefined in the browser.

// Server Component or API route
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);   // ✅ server only

// Client Component
const stripeKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;  // ✅ exposed to browser
const secret = process.env.STRIPE_SECRET_KEY;  // ❌ undefined in browser

Validating Environment Variables at Startup

Missing environment variables cause confusing errors deep in your code. Validate at startup with zod:

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

const envSchema = z.object({
    DATABASE_URL: z.string().url(),
    JWT_SECRET: z.string().min(32),
    STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
    NEXT_PUBLIC_APP_URL: z.string().url(),
    NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
});

const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
    console.error("❌ Invalid environment variables:");
    console.error(parsed.error.flatten().fieldErrors);
    process.exit(1);
}

export const env = parsed.data;
// Use in your code
import { env } from "@/lib/env";

const stripe = new Stripe(env.STRIPE_SECRET_KEY);

Now your app fails fast at startup with a clear error message if anything is missing, rather than crashing mysteriously at the first request that needs the variable.

Environment-Specific Files

Next.js loads .env files in this order (later files override earlier):

  1. .env — shared defaults
  2. .env.local — local overrides, not committed
  3. .env.development / .env.production / .env.test — environment-specific
# .env — shared defaults across all environments
NEXT_PUBLIC_APP_NAME=AiTechWorlds

# .env.development — automatically loaded in development
DATABASE_URL=postgresql://localhost:5432/myapp_dev
NEXT_PUBLIC_APP_URL=http://localhost:3000

# .env.production — automatically loaded in production build
NEXT_PUBLIC_APP_URL=https://myapp.com

Only commit .env and environment-specific files if they contain no secrets. Use the platform's secret management for production secrets.

Node.js / Express

// Install dotenv
npm install dotenv

// src/index.ts — load as the very first line
import "dotenv/config";

// Or with require syntax
require("dotenv").config();

// Now process.env.DATABASE_URL etc. work

For multiple environments, use different .env files:

import dotenv from "dotenv";

dotenv.config({
    path: `.env.${process.env.NODE_ENV ?? "development"}`
});

Accessing Secrets in Production

For production secrets on Vercel:

  1. Dashboard → Project → Settings → Environment Variables
  2. Add each variable and set scope (Production / Preview / Development)
  3. Redeploy for changes to take effect

For teams using AWS, consider AWS Secrets Manager or Parameter Store. For Kubernetes, use Kubernetes Secrets or Vault.

Rotating Secrets

When a secret is compromised or needs to be changed:

  1. Generate a new value (new API key, new password)
  2. Add it to your production environment as the new value
  3. Redeploy your application
  4. Revoke the old value in the service dashboard (Stripe, your database, etc.)
  5. Update .env.example if the variable format changed

Keep a record of which services each key gives access to. If a .env is accidentally committed:

# Remove from git history (after rotating the credentials)
git filter-branch --force --index-filter \
    "git rm --cached --ignore-unmatch .env" \
    --prune-empty --tag-name-filter cat -- --all

# Force push (coordinate with your team)
git push origin --force --all

This rewrites history, but anyone who already cloned the repo may have the secrets cached. The only safe action is to treat the credentials as compromised and rotate them immediately.

Checklist for Every Project

✅ .env.example committed with all variable names, no values
✅ .env* in .gitignore (check with git status before every commit)
✅ Production secrets set on the deployment platform, not hardcoded
✅ NEXT_PUBLIC_ prefix only for variables safe to expose to users
✅ Startup validation that fails loudly if variables are missing
✅ No secrets in error messages, logs, or API responses
✅ Different values for dev, staging, and production

Next lesson: Building a full-stack blog — putting everything together in a real project.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!