SYS://FIELD-REPORT-10

Dev and Prod Share One Database. There Is No Dev.

The app worked. It looked finished. It had real signups. And it had exactly one place to run: production. There was no staging, no preview, no scratch environment — the thing the owner called “dev” was pointed at the same Supabase project serving live users. Run a migration to try something out, and you’re trying it out on everyone. Here’s the teardown, worst finding first.

This is a representative audit — the exact shape I hit over and over on apps built with Bolt, v0, Cursor, and Claude Code. Names and paths are changed, but every finding below is real-shaped and drawn straight from the kind of report the launchworthy skill produces. Call the app routeledger: a Remix + Supabase expense tracker, single Supabase project, deployed to a VPS, already collecting real users’ receipts.

Most teardowns on this blog open with a CRITICAL. This one doesn’t, and I’m going to be honest about that instead of inflating it. The worst finding here is a [HIGH]. It is still the thing that ends the app.

0/5
domains passing
0
critical findings
4
high findings

Zero criticals does not mean safe. It means nothing here will leak your users’ data to a stranger — the auth and RLS work was actually done right, which is rarer than it should be. What it means instead is that routeledger has no safe way to change. And an app you can’t change without gambling on live data is an app that breaks the first time its owner tries to improve it.

The finding that ends the app: there is no dev

Every framework tutorial says “test it in dev first.” routeledger’s problem is that “dev” and “prod” are the same database.

The Remix app reads its Supabase connection from environment variables. There is one set of them. .env on the developer’s laptop and the environment on the VPS both point at the same project ref:

# .env  (laptop AND server — identical)
SUPABASE_URL=https://abcd1234.supabase.co
DATABASE_URL=postgres://[email protected]:5432/postgres

So when the owner opens the project locally to “try a schema change,” supabase db push runs against abcd1234 — the live one. There is no second project to catch the mistake. A dropped column, a bad NOT NULL backfill, an UPDATE with a wrong WHERE — it lands on real users’ receipts instantly, with no rehearsal and, as we’ll see, no way back.

HIGH · CAUGHT

Only one environment — dev migrations run against live data

Dev and production share a single Supabase project. A careless migration or query you run to “test something” rewrites the rows real users depend on. The shared database does not know it is a test. There is no staging copy to make the mistake safe.

Two more findings sit right next to it, and together they turn a mistake into an unrecoverable one.

The [HIGH] production points at localhost: the deployed app’s .env still carried a REDIS_URL=redis://localhost:6379 and an OAuth callback of http://localhost:3000/auth/callback copied from the laptop. In production those resolve to the server’s own loopback, not the developer’s machine — so the session cache silently misses and OAuth round-trips 404 for real users. It works on the laptop, which is exactly why it shipped.

And the [HIGH] no documented rollback: this is on a VPS. There is no Vercel/Netlify/Cloudflare instant-rollback button here, no previous image retained, no written “if the deploy is bad, do X.” So the moment the bad migration lands on the shared database, there is no previous-good to return to. The two [HIGH]s compound: one place to make the mistake, no place to undo it.

THE FIX · IN ORDER

Give yourself a second environment, then a way back

  1. Create a separate Supabase project for production, or use Supabase’s branching so preview work runs on a copy. Move the live credentials into the server’s environment only; keep a distinct dev project ref in your laptop .env. Now supabase db push in dev can’t touch live rows.
  2. Purge localhost from the production env. Every REDIS_URL, callback URL, and base URL in the deployed config must be a real production host. Grep the built app and the server env for localhost and 127.0.0.1 before shipping.
  3. Write the rollback down. On a VPS that means: tag each deploy, keep the previous release directory (or image) so you can symlink back, confirm Supabase daily backups are on and note the retention, and put the exact “roll back” commands in the README. A rollback you have to invent mid-incident is not a rollback.

Verify by hand Open the deployed server’s environment and diff it against your laptop .env. They must not be identical — same-file is the smell. Confirm the two Supabase project refs differ, and that no production value contains localhost. Evidence: paste the two SUPABASE_URL lines side by side; they should not match.

The false alarm: “it’s just an MVP”

Here’s the credibility move, and it’s not about a key this time — routeledger got its secrets right. The false positive is a mindset, the one that would talk you out of every fix above.

Point a generic review at an app this small and the reflex — often from the owner themselves — is to wave it off:

FALSE POSITIVE

It's just an MVP, one environment is fine for now

This is the excuse that feels like maturity and is actually the risk. An MVP with real signups is a real product to the person whose receipts are in it. The shared-database migration does not know it’s an MVP — a dropped column deletes real data at the same speed it would at a Series B. “One environment is fine for now” quietly means “the first time I improve this app, I’m rolling dice on live users,” and there’s no undo behind it.

“It’s an MVP” is a good reason to keep the feature set small. It is not a reason to skip the one thing that lets you change the app without risking the people already using it. Environment separation isn’t scale infrastructure — it’s the seatbelt you put on before the first drive, not after the first crash.

The tell of a shallow review is treating “small app” as “low stakes.” The stakes aren’t set by your user count. They’re set by whether the data in there is real. It is.

ASK YOURSELF

If a migration you ran to “just test something” deleted a column tonight, whose data would be gone — and could you get it back before the first user noticed? If the honest answer is “my real users, and no,” you don’t have an MVP problem. You have a one-environment problem, and it’s a [HIGH].

The rest of the punch list

The environment work is the blocker. The rest is ranked so the scope stays honest — none of it is a secret leak, and I’m not going to pretend it is.

FIX THIS WEEK

After the environment split

  1. [MEDIUM] No preview / branch deploys. Every change goes straight to the one live box with no throwaway URL to click through first. Vercel, Netlify, and Cloudflare hand you preview deploys per branch for free; a VPS doesn’t, so a second environment plus a staging branch is the manual equivalent worth building.
  2. [MEDIUM] Hardcoded production URLs in source. app/lib/config.ts:12 has const API_BASE = "https://routeledger.app/api" compiled straight into the bundle instead of read from an env var. It works until the day the domain or the API host changes, and then it’s a code edit and a redeploy instead of an env flip. Move host and base URLs into environment config.

Four [HIGH]s, two [MEDIUM]s, zero criticals. Note the shape: the danger here isn’t exposure, it’s fragility. A flat list reads as six equal chores. Worst-first reads as “give yourself a second environment and a way back this week — the two mediums are next.”

Why this keeps happening

None of this means routeledger’s owner did anything foolish, or that the builder was careless. AI tools are extraordinary at getting you to one working environment — the one on your screen, where you’re the only user and nothing has broken yet. A single environment is the natural output of “make it run,” because a second one is invisible until the first time you need it, which is always the worst possible time to discover you don’t have it.

That gap — between it runs on my machine and I can change it without gambling on real users — is the whole reason launchworthy exists. It plays bouncer at the door of production: it checks whether dev and prod are actually separate, whether anything in the deployed config still points at localhost, and whether there’s a written way back from a bad deploy. Then it hands you the fix, not just the finding.

The scariest-sounding audit results are usually the leaks. routeledger didn’t have one. Its real exposure was quieter and, for the person about to hit deploy, more likely: the first time they tried to make the app better, there was nowhere safe to do it.