Somebody tests a "delete inactive accounts" script and forty real customers lose their logins. The script was correct. It ran against the wrong copy of the database.
This is the most expensive beginner mistake in the whole chapter, and it is prevented by one habit rather than by any tool.
The idea
An environment is one running copy of your app together with the data and settings it points at. The code is the same in all of them. What differs is which database it talks to, which API keys it holds, and who is allowed to see it. Your machine is one environment. The live site your users visit is The environment real users are actually using. Its data is real, so mistakes there are permanent.. A private copy of production, with fake data, used to check a change before real users get it. is an optional third one in between.
LOCAL STAGING PRODUCTION
(your laptop) (private URL) (myapp.com)
------------ ------------ ------------
fake data fake data REAL USER DATA
test API keys test API keys live API keys
audience: you audience: you audience: everyone
break it freely break it freely breaking it is an incident
| | |
+---- same code ------+------- same code -----+
the only irreversible box is the one on the rightHow it works
- The separation lives in configuration, not in code. One codebase reads
DATABASE_URLfrom its environment and gets a local Postgres on your laptop, a throwaway Supabase project in staging, and the real one in production. The application never knows which it is. - Most hosts give you preview environments for free. Vercel and Netlify build every git branch at its own URL automatically, so "does this branch work" gets answered before you merge.
- Databases are the part you have to separate deliberately. Nothing stops your local app from
connecting to the production database, and pointing a local
.envat production is the single most common way this goes wrong. - Third-party services have their own split. Stripe has test keys and live keys. Resend has a test mode. Using the live key locally means a test checkout charges a real card.
What to do
- Give local development its own database from day one. Run Postgres locally with Docker, or
create a second free Supabase project called
myapp-dev. It costs nothing and it is the whole defence. - Say it out loud when you brief your assistant. "This runs against the local dev database. Do
not use the production connection string." Then check what it did before you run anything with
DELETE,DROPorUPDATEin it. - Add staging only when you have someone to protect. With no users, your local machine is your staging. Once real people depend on the app, or once you have a paid tier, a staging environment stops being ceremony.
- Seed staging with fake data, not with a copy of production. Copying the production database into staging is a common shortcut and it moves real names, emails and addresses into a system with weaker protection. If you must copy, scramble the personal fields first.
Where it breaks
Three environments cost three sets of everything: three databases, three sets of keys, three things that drift. A solo builder with no users who insists on full staging usually ends up with a staging copy that has been broken for a month and tells them nothing. Skipping it is a legitimate choice as long as you know you are trading it for care at the moment of deploy.
Staging also lies in a specific way. It has small, clean, fake data, so a query that takes eleven minutes against a real table finishes instantly there, and a bug that only appears with a real volume of records will not show up until production. A green staging run raises your confidence without ever finishing the argument.