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:
-
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.
-
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. -
Write three local
.envfiles —.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. -
Add an
APP_ENVvariable to all three environments and log it on app startup so you can always see at a glance which environment you're running against. -
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
.gitignoreblocks all.env*files and agit log --all -- .env.productionreturns 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. 🏛️