Every broken deployment has a story. Here is one of the most common ones.
A developer builds a payment flow over a weekend. They test it thoroughly in their local app using Stripe's test mode. The card 4242 4242 4242 4242 goes through cleanly. They push to production on Monday morning, confident it works. Within an hour, real users are complaining that payments are failing. The developer checks the logs and finds the problem immediately: the Stripe test key (sk_test_xxx) is still set in production. Real credit cards cannot be processed with a test key. Every transaction is silently rejected.
The fix is a one-line change. But the real lesson is architectural: development and production should never share configuration. That is what environments are for.
What an Environment Is
An environment is a separate, isolated instance of your application running with its own configuration, data, and infrastructure. When you open your project on your laptop and hit npm run dev, you are running in one environment. When a real user visits your site from their browser, they are hitting a completely different environment β different server, different database, different API keys.
The separation is intentional. It means a bug you introduce at 2 AM does not immediately break the product your paying customers are using. It means you can test a new feature against realistic data before it is visible to the world. It means the test credit card you just ran through checkout does not appear on a real bank statement.
Professional software teams typically maintain three environments.
The Three Standard Environments
Development (dev)
Development runs on your machine. It is yours to break. You can delete the database, push half-written code, enable verbose logging that prints every SQL query to the console, and crash the app twenty times before lunch. None of that affects anyone else.
Development typically uses:
- A local database (PostgreSQL running on
localhost:5432, or SQLite in a file) - Fake or seeded data β not real user information
- Test API keys that do not process real money
- Hot reload so the app refreshes automatically as you save files
- Debug tools, error overlays, and source maps turned on
The goal of the development environment is speed. You want fast feedback loops. You do not care about performance, uptime, or data integrity the way you would in production.
Staging
Staging is the environment most beginners skip β and missing it is where expensive mistakes happen.
Staging mirrors production as closely as possible: same infrastructure, same build configuration, same environment variables (swapped for staging-specific values), same Docker image if you are using containers. But it is not public. Only your team can access it, usually behind a password or a VPN.
Staging is used for:
- QA testing β your quality assurance team (or you, playing that role) runs through every user flow before a release
- Testing with real-ish data β staging often uses an anonymised copy of the production database, so you are testing against realistic volume and variety
- Catching integration issues β third-party APIs behave differently in staging than they do on
localhost(webhooks, redirects, CORS headers) - Final review β product managers and designers sign off on a feature in staging before it goes live
The rule: if something passes in staging, it is ready for production. If you skip staging and go straight from dev to production, you are making a bet that your localhost environment perfectly matches the live server. It almost never does.
Production (prod)
Production is the live application. Real users, real data, real money. This is the environment where mistakes have consequences.
Production deployments follow strict rules at most companies:
- Changes go through code review before they are merged
- Automated tests must pass in CI before a deploy is allowed
- Database migrations are run in maintenance windows or with backwards-compatible patterns (never drop a column without a two-step process)
- Rollbacks are planned before deployments happen, not after
In production, debug mode is off. Error details are hidden from users (but logged to a monitoring service). Performance matters. Uptime matters.
The core principle: production is conservative. You do not experiment in production.
Environment Variables: The Glue
The mechanism that makes environments work is environment variables β key-value pairs that are set outside your code and change per environment.
Instead of writing this in your code:
const stripe = new Stripe('sk_live_my_real_key_that_charges_money');
You write:
const stripe = new Stripe(process.env.STRIPE_KEY);
And then you define STRIPE_KEY separately in each environment. In development it is the test key. In production it is the live key. Your code never changes β only the value injected at runtime.
A .env file for development might look like this:
DATABASE_URL=postgresql://localhost:5432/myapp_dev
STRIPE_KEY=sk_test_xxx
ANTHROPIC_API_KEY=sk-ant-xxx
NEXT_PUBLIC_APP_URL=http://localhost:3000
And the production environment would have:
DATABASE_URL=postgresql://prod-server/myapp_prod
STRIPE_KEY=sk_live_xxx
NEXT_PUBLIC_APP_URL=https://myapp.com
The database URL points to different servers. The Stripe key switches from test to live. The app URL is the real domain instead of localhost. Same code, completely different runtime behavior.
The .env File in Next.js
If you are building with Next.js, the standard pattern is to use .env.local for your local development secrets. Next.js loads it automatically when you run next dev.
The naming convention matters:
.envβ checked into git, used for non-secret defaults.env.localβ never committed, used for local secrets (overrides.env).env.productionβ checked into git, used for non-secret production defaults.env.production.localβ never committed, used for production secrets
Variables prefixed with NEXT_PUBLIC_ are exposed to the browser. Everything else stays server-side only. Never put a secret API key in a NEXT_PUBLIC_ variable β it will be bundled into the JavaScript file that every visitor downloads.
Why .env Must Be in .gitignore
The single most important rule about .env files: they must never be committed to version control.
Add this to your .gitignore:
.env
.env.local
.env.*.local
If a .env file with real API keys lands in a public GitHub repository, automated bots find it within minutes. Those bots are scanning GitHub continuously for leaked credentials. Once a key is exposed in git history, you must rotate it immediately β not delete the file, because the key is still in the commit history. Rotate it. Then delete the file and add it to .gitignore.
Feature Flags: Deploying Without Releasing
Some teams take environment separation further with feature flags. A feature flag is a configuration toggle that lets you deploy code to production but hide it from most users. You might deploy a new dashboard design but only show it to internal employees until it is polished. Or you might roll it out to 5% of users, watch the error rate, and expand the rollout if everything looks healthy.
Feature flags decouple deployment (pushing code to the server) from release (making a feature visible to users). This is safer than combining both into one step.
Tools like LaunchDarkly, Unleash, and PostHog feature flags implement this pattern. For smaller projects, a simple environment variable or a database row can serve as a basic flag.
How Code Flows Between Environments
A typical CI/CD (Continuous Integration / Continuous Deployment) pipeline works like this:
- You push a branch to GitHub and open a pull request
- The CI system (GitHub Actions, CircleCI) runs your automated tests
- If tests pass, the branch is deployed automatically to the staging environment
- Your team reviews the feature in staging
- The pull request is merged to the main branch
- The CI/CD pipeline deploys the main branch to production
Each step is a gate. Broken tests block deployment to staging. A failed staging review blocks the merge. The goal is to catch problems as early as possible β in development, then in staging β so production stays clean.
If you are learning git and GitHub and want to understand how to push your code in the first place, the beginner guide to Git and GitHub covers that foundation.
Common Mistakes Beginners Make
Running database migrations directly in production. Always test migrations in staging first. A migration that drops a column, renames a table, or changes a data type can break a running app in unpredictable ways. Staging is where you discover that.
Using production API keys in development. If you use your live Stripe key locally and accidentally process a test charge, you may create accounting noise, trigger fraud detection, or incur unexpected fees. Always use test keys in development.
Committing secrets to git. Covered above, but worth repeating. Add .env.local to .gitignore before you create the file, not after.
Not having a staging environment at all. This feels fine until the first time you deploy a bug that was obvious when someone looked at it in a browser. Staging is cheap (or free on most platforms). The bugs it catches are not.
Assuming localhost behavior matches production. HTTPS redirects, cookie Secure flags, CORS headers, and environment-specific webhook URLs all behave differently locally. Staging with a real domain reveals these issues before production does.
Quick Reference
| Development | Staging | Production | |
|---|---|---|---|
| Who uses it | You (developer) | Your team, QA | Real users |
| Data | Fake / seeded | Anonymised copy of prod | Real user data |
| API keys | Test keys | Test or staging keys | Live keys |
| Errors shown | Full detail, stack traces | Internal only | Hidden from users |
| Stability needed | Low β break freely | Medium | High |
| Deployments | Constant | On each PR merge | Deliberate, reviewed |
Understanding environments is one of those concepts that clicks quietly but then suddenly explains a dozen things that were confusing before β why .gitignore exists, why there are multiple database URLs in a config file, why the QA team has a separate website to test on. Once you see the separation, you start building with it from day one.