There is one database in your project, and it’s the one your customers are on. Every change you push lands on it; when a coding agent runs a migration, it runs against rows a paying user is looking at right now. The tools set it up this way: one project that is the app, one database, no staging, and a push to main that deploys on its own.

I can put a number on how normal that is. Of the 21 third-party apps in the AxonBuild audit corpus, at least 17 had no deploy gate at all, meaning every push went straight to production with nothing checking it first. Then I ran the same audit on my own five production apps, and all five failed the same check. The method has no mercy setting.

This setup fails in a specific way: something ships broken, you reach for the rollback button, and the button fixes the wrong half of the app. The code reverts. The database change stays. Everything below is about that day, starting with why the advice that says staging is optional was written for someone else, and ending with the one-time setup that closes each gap.

One app, one database, no staging environment

The default deploy has three properties, and the demo shows none of them. Every push to main goes live on its own, so the first thing to run your new code in production is a customer’s next request. Nothing gates the push; several of the audited apps had explicitly disabled their own type and lint checks at build time, so code that no longer compiled cleanly still shipped. And because the only database is the live one, trying anything at all means testing on the production database.

While you’re building, the setup is pure upside, the fastest loop from prompt to deployed change, which is why every tool scaffolds it. The cost arrives later, as drag: the 2024 DORA report estimated a 7.2% drop in delivery stability as AI adoption climbed. AI makes it easy to ship bigger changes faster than anything reviews them, and speed with no gate converts into incidents.

”You might not need staging” is true, for teams that aren’t you

Search for whether you need a staging environment and the advice that ranks mostly says no. ivelum’s “You might not need staging” argues that what you verify on staging is never quite what ships, and that a disciplined team does better testing in production behind feature flags, canary releases, and dark launches. In the Hacker News threads on the practice, engineers who ship without staging keep naming the same equipment: feature flags, 1% rollouts, review on every change.

Inside their context, this advice is right; staging really does drift, and large teams really have replaced it with flags and canaries. But collect what every one of those authors assumes you already have:

  • A CI pipeline that blocks the merge when tests fail. Which assumes tests: at least 23 of 26 apps in the corpus had zero working automated tests.
  • Feature flags, so a bad change can be switched off without touching the deploy.
  • Observability good enough to notice a failing canary before customers do. The corpus number on that wiring is below, and it isn’t comforting.
  • A rollback path someone has exercised at least once, databases included.

The people retiring their staging environments already deploy straight to prod dozens of times a day, behind flags, in front of dashboards, with a drilled rollback; staging was redundant for them four layers over. If your deploy is a push with nothing in front of it and nothing watching behind it, you are the reader staging was invented for.

One more note from reading those pieces closely: ivelum treats database migrations as the hard part staging still handles, and offers nothing in its place under the test-in-production model. I haven’t found a kill-your-staging piece that solves that problem; if one exists I want to read it. The database is also where the corpus says the one-project setup does its damage, so the next two sections stay there.

Testing changes on your customers’ database

With no staging environment, every change you try runs against the same rows real customers depend on. There is no copy to break first.

Staging is where experiments are cheap: seed junk data, run a destructive query, let an agent take a swing at a migration, reset it if the attempt goes badly. With one project, every experiment lands on live data, and the data-loss bugs that hide in an AI-built app get an order of magnitude worse when the only database they can reach is the production one.

The starkest version I’ve audited was a medical app holding real patient records. It had no staging environment, so every change was tried against the production database, and the schema lived in a DROP-then-CREATE script: applying it meant deleting the tables first. The same deploy step pruned the previous Docker image, which left the one environment that existed with no older version to roll back to. I read that deploy script twice before writing the finding, because I wanted to have misread it. In the demo, none of this was visible. The app worked.

A rollback reverts the frontend, not the database

Managed hosts make rollback look like one button: redeploy the previous version. That reverts your code. It does not revert your data, and the change that takes an app down is usually in the data.

You can’t roll back a database migration by redeploying old code. Migrations run forward, and they reverse only if someone wrote the reverse path and ran it at least once.

-- This ran on deploy and dropped the column in production:
ALTER TABLE orders DROP COLUMN legacy_total;

-- "Rolling back" redeploys the old code, which still reads legacy_total.
-- Nothing re-creates the column. Every value it held is already gone.

Rolling the app back now points yesterday’s working code at a database that no longer matches it, a second outage stacked on the first. In the July 2025 Replit incident, an AI coding agent deleted a company’s production database during an explicit code freeze, then told the founder a rollback was impossible; the data came back through manual recovery. The vendor’s announced fix was automatic separation between development and production databases. The isolation that wasn’t there is the entire lesson.

Looks fine
Actually fine
The host has a one-click rollback to the last deploy
That rollback re-points the code; a schema change it made to the database stays applied
Migrations run cleanly on every deploy
Every migration has a down-path you have run once, so it reverses when you need it to
A nightly backup runs and the dashboard says it succeeded
You have restored that backup into a scratch database and watched the data come back
Looks fine
The host has a one-click rollback to the last deploy
Migrations run cleanly on every deploy
A nightly backup runs and the dashboard says it succeeded
Actually fine
The host has a one-click rollback to the last deploy
That rollback re-points the code; a schema change it made to the database stays applied
Migrations run cleanly on every deploy
Every migration has a down-path you have run once, so it reverses when you need it to
A nightly backup runs and the dashboard says it succeeded
You have restored that backup into a scratch database and watched the data come back

A rollback reverts the frontend. The database change that broke you stays applied until you reverse it yourself.

The gap between those columns closes in unglamorous steps: write the down-migration and run it once on staging; restore one backup into a scratch database and watch the rows come back. If you’d rather have evidence than a feeling, a Beyond the Demo Audit walks your deploy path from push to rollback and reports which of those claims your app can back up.

Know before your customers do

Without error tracking, an uptime check, and a spend cap, the first thing to tell you production is down is a customer. The second is an invoice.

The corpus puts a number on the missing wiring: 17 of the 21 third-party apps recorded errors nowhere. When a user hits a bug in one of those apps, nothing logs it and nothing alerts anyone; it just disappears. The failures that worry me most don’t even crash: they return 200 OK while something like checkout quietly fails. Error tracking and an uptime check are minutes of setup each, and they turn that silence into a message on your phone before the support ticket arrives.

The invoice is the other blind spot. In June 2024 the artist platform Cara grew from 40,000 to 650,000 users in a week, and its founder found a $96,280 Vercel bill for that week. In February 2024 a developer’s static side project took a bandwidth flood and got a $104,000 Netlify bill, waived only after the story spread. Neither builder chose that spend; no cap or alert stood in front of it. A billing alert and a hard spend limit are a setting rather than an architecture.

The one-time setup

The fix is a short list you work through once, and it stays done. On an app that’s already live it costs an afternoon, and after that afternoon a bad deploy becomes a story you tell instead of an outage you eat.

  1. 01 Stand up a separate staging environment with its own database and its own secrets, so no test ever runs against production data.
  2. 02 Commit an .env.example with every variable name and no values; keep real secrets in your host’s secret manager, one set per environment; put .env in .gitignore from the first commit.
  3. 03 Add a CI gate that runs your tests and lint on every pull request and blocks the merge when they fail.
  4. 04 Write a down-migration for every schema change, and run it once on staging so you know it reverses cleanly.
  5. 05 Take a backup before every production migration, and restore it once into a scratch database to prove the backup actually works.
  6. 06 Wire up error tracking and an uptime check that alert you, so a dashboard reports a failure before a customer does.
  7. 07 Set a billing alert and a hard spend cap on your host and database, so a traffic spike can’t run up a five-figure bill before you notice.

The .env.example is the smallest habit on the list and carries the most weight:

# .env.example, committed to the repo: names only, values always empty.
DATABASE_URL=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=

The committed file documents what the app needs; the real values live in the host’s secret manager, one set per environment: config in the environment, never in the code. Get this right on commit one and a leaked key is a problem you never have.

Common questions about staging and rollbacks

Do I need a staging environment?

If you already run CI that blocks failing builds, ship behind feature flags, and trust your observability, maybe not; the teams that skip staging lean on exactly those. If your app deploys on push with no tests and no error tracking, then yes: a second project with its own database, where migrations, upgrades, and agent experiments happen before customers meet them, is the cheapest safety you can add. It’s one slice of what launch readiness actually checks, and the slice worth doing first, because it protects every change you make after it.

Does a one-click rollback undo a database migration?

No. Redeploying the previous version restores the previous code and nothing else. A schema change your deploy applied stays applied until you run a reverse migration, and data a migration destroyed comes back only from a backup you can restore. Both of those are claims you can test on a quiet afternoon, on staging, with a real schema change. The day something breaks is the wrong day to run that drill for the first time.