Skip to main content
DevOps, Deployment & Operations
🚀 DevOps & OpsLesson 1 of 13

Environments: Dev, Staging, Prod

Dev, staging, prod — why they differ, and why mixing them up burns you.

Environments: Dev, Staging, Prod

Stage 3 · DevOps, Deployment & Operations · B.U.I.L.D. letter: D

You built something real. Users are counting on it. The line between "I'm testing" and "I broke production" is the most important line in software engineering — and it's drawn by your environment setup.


⚠️ The vibe trap

When you're moving fast and everything works on your machine, it's tempting to push straight to your live app and see what happens. That works — until the day it doesn't, and "what happens" means a real user hits a broken checkout, a corrupted record, or a 500 page at 2 a.m. Vibing is how you ship fast; environments are how you stay alive while you do it. The two are not enemies — this lesson is how you combine them.


🏠 The Three-Environment Model

Every serious app has at least three environments. Think of them as three different cities that your code visits on its way to users.

┌─────────────────────────────────────────────────────────────┐
│  ENVIRONMENT TABLE                                          │
├──────────────┬──────────────┬──────────────┬───────────────┤
│              │ DEV (local)  │ STAGING      │ PRODUCTION    │
├──────────────┼──────────────┼──────────────┼───────────────┤
│ Who uses it  │ You, alone   │ Your team,   │ Real users    │
│              │              │ QA, demos    │               │
├──────────────┼──────────────┼──────────────┼───────────────┤
│ Database     │ Local or dev │ Separate     │ The real one  │
│              │ Supabase     │ Supabase     │ — never share │
├──────────────┼──────────────┼──────────────┼───────────────┤
│ Data         │ Seed data,   │ Anonymized   │ Live user     │
│              │ fake users   │ copy or seed │ data          │
├──────────────┼──────────────┼──────────────┼───────────────┤
│ Deployments  │ npm run dev  │ Auto-deploy  │ Manual or     │
│              │              │ on PR merge  │ gated CI/CD   │
├──────────────┼──────────────┼──────────────┼───────────────┤
│ Failures OK? │ Yes, always  │ Yes, that's  │ No. Never.    │
│              │              │ the point    │               │
└──────────────┴──────────────┴──────────────┴───────────────┘

Mental model: Dev is your sketchpad. Staging is the dress rehearsal. Prod is the opening night — the audience paid for tickets.

Why it matters: Most "it works on my machine" disasters are really "it works against my dev database." Staging exists to catch the gap between your assumptions and reality.

Common mistake: Having only two environments — your laptop and prod. You'll skip staging because it "feels like extra work" right up until the night you need it most.


🔑 Config & Secrets Per Environment

Each environment needs its own set of configuration values. Never share a database URL, API key, or secret between environments.

# .env.development  (your laptop — never commit this file)
DATABASE_URL=postgresql://postgres:devpassword@localhost:5432/myapp_dev
NEXT_PUBLIC_API_URL=http://localhost:3000
STRIPE_SECRET_KEY=sk_test_abc123_your_test_key
SENDGRID_API_KEY=SG.dev_key_here
APP_ENV=development

# .env.staging  (injected by your CI/CD platform — also never committed)
DATABASE_URL=postgresql://user:pass@staging-db.supabase.co:5432/myapp_staging
NEXT_PUBLIC_API_URL=https://staging.myapp.com
STRIPE_SECRET_KEY=sk_test_abc123_your_test_key
SENDGRID_API_KEY=SG.staging_key_here
APP_ENV=staging

# .env.production  (injected by your platform — never in the repo, ever)
DATABASE_URL=postgresql://user:pass@prod-db.supabase.co:5432/myapp_prod
NEXT_PUBLIC_API_URL=https://myapp.com
STRIPE_SECRET_KEY=sk_live_REAL_MONEY_KEY
SENDGRID_API_KEY=SG.production_key_here
APP_ENV=production

Mental model: Your .env.* files are the addresses and passwords for each city. Different city, different keys. The prod keys unlock real money, real email, real data — they must never land in your dev terminal or your git history.

Why it matters: A test run against your prod database with a DELETE that has a bug in the WHERE clause is not a "dev mistake." It is a data loss incident. The database URL is what makes the difference.

Common mistake: Committing a .env file to git. Add .env* to .gitignore on day one. Check your .gitignore right now if you haven't.


🪜 Promoting a Build Through Environments

Code should travel in one direction: dev → staging → prod. You never skip staging for a "quick fix" — that's the most dangerous sentence in software.

# The healthy promotion flow

# 1. Work locally, run dev server
npm run dev
# test your changes against local/dev DB

# 2. Open a pull request → CI runs tests → merge to main
# staging auto-deploys (Vercel, Render, Railway — they all do this)

# 3. Verify on staging: hit the URL, click through the feature, check logs
open https://staging.myapp.com

# 4. When staging is green, promote to prod
# Option A: a manual Vercel prod deploy
npx vercel --prod

# Option B: merge staging → prod branch (or tag a release)
git checkout main
git merge staging
git push origin main
# CI/CD deploys to prod automatically

Mental model: Think of it like food safety. You don't go from raw ingredients straight to the customer's plate. You have a prep station (dev), a pass window where the chef checks it (staging), and only then does it go to the table (prod).

Why it matters: Staging gives you a second chance to catch something your tests missed — a misconfigured environment variable, a migration that behaves differently against real-scale data, a third-party integration that only misbehaves in non-test mode.

Common mistake: "Just this once, I'll push straight to prod — it's a one-line change." The one-line change that broke everything is a genre of engineering horror story. The genre is enormous.


🌿 Preview / Ephemeral Environments Per PR

Modern deployment platforms give you a superpower: a fresh, temporary environment for every pull request. This is called a preview environment (Vercel calls them Preview Deployments; Netlify calls them Deploy Previews).

# When you push a branch or open a PR, Vercel automatically:
# 1. Builds your app from that branch
# 2. Deploys it to a unique URL
# 3. Posts the URL in your PR

# Example preview URL format:
# https://myapp-git-feature-auth-yourteam.vercel.app

# You can configure which env vars preview deployments get
# in your Vercel dashboard → Project Settings → Environment Variables
# Set them for "Preview" (not Production, not Development)

# To check which environment your Next.js app thinks it's in:
echo $VERCEL_ENV      # "preview", "production", or "development"
echo $APP_ENV         # whatever you set in your env vars

Mental model: Ephemeral environments are like pop-up restaurants. They exist for one purpose — let someone taste the food from this specific PR — and then they disappear when the PR closes. No lingering state, no cleanup required.

Why it matters: Preview environments let non-engineers (designers, stakeholders, QA) review a feature on a real URL before it ever reaches staging or prod. Feedback is faster, bugs are caught earlier, and you stop being the gatekeeper for "can I see what you built."

Common mistake: Giving preview environments access to your staging database. Create a dedicated preview database (or use a database-branching service like Supabase branching) so a preview deployment can't dirty your staging data.


🛠️ Your Mission

Set up proper environment separation for your app. If you're on Vercel (likely), this means:

  1. Audit your current environment variables in your Vercel dashboard. Are any of them the same across Development, Preview, and Production? Fix that — each tier should have its own values.

  2. Create a separate database for staging. If you're using Supabase, spin up a second project (free tier is fine) and call it myapp-staging. Point your staging/preview env vars at it.

  3. Write three local .env files.env.development, .env.staging, .env.production — even if staging and prod are only filled with comments and placeholders for now. Add all three to .gitignore.

  4. Add an APP_ENV variable to all three environments and log it on app startup so you can always see at a glance which environment you're running against.

  5. Open a test PR and verify that Vercel creates a Preview deployment automatically. Click the preview URL and confirm it works.


✅ You're done when…

  • You have verified your app against the Production-Readiness Checklist and confirmed each environment has its own database URL (dev, staging/preview, prod — no sharing)
  • Your .gitignore blocks all .env* files and a git log --all -- .env.production returns nothing
  • Pushing a branch to GitHub creates a Vercel Preview deployment with its own unique URL
  • Your staging environment points at a database that does not contain real user data
  • You can read APP_ENV (or equivalent) from your app's logs and confirm it prints the correct environment name in each context

➡️ Next: Deployment Without Mystery.

Build It Right, Or Don't Build It At All. 🏛️

Always-on rigor toolkit

🏛️ Build It Right, Or Don't Build It At All.