Notes ·

Works in preview, breaks in production: a checklist for AI‑built apps

The preview is a kind place. It has your keys, a database nobody else uses, and one user: you. Production has strangers, real money and a different server. Here is what I check, in this order, when an app built with Lovable, Bolt, v0, Replit, Cursor or Claude Code works in the preview and not live.

1. Environment variables

The most common cause of a blank page after deploy. The builder kept your settings for you; the new host has never heard of them.

  • List every variable the code reads. Search for process.env and import.meta.env. Each one must be set on the host, for production and for any preview deploys.
  • Variables with a public prefix (VITE_, NEXT_PUBLIC_) are written into the JavaScript at build time. Changing one on the host does nothing until you build again.
  • Payment and email providers give you test keys and live keys. Stripe’s start with sk_test_ and sk_live_. The preview ran on test keys; production needs live ones, and live webhooks pointing at your real domain.

2. Secrets in the browser and in the repo

Anything the browser downloads is public, prefix or not.

  • Open the deployed site, then the browser’s developer tools, and search the loaded scripts for secret, sk_, service_role and your providers’ names. A key you find there belongs to everyone.
  • Calls that need a secret key (payments, AI models, sending email) must go through a server function that holds the key. The browser calls your function, never the provider.
  • Deleting a committed .env file doesn’t remove it from the git history. Rotate every key that was ever in it, then turn on secret scanning (GitHub has it built in; gitleaks runs anywhere).

3. Who can read which rows

With Supabase or Firebase, the browser talks to the database directly. The public key is meant to be public, so the only thing between a visitor and your tables is the database’s own rules.

  • Supabase: check that row-level security is on for every table, and that each policy ties rows to the signed-in user (auth.uid()). A table created from SQL may start with it off. Firebase: read your security rules and look for anything still open from test mode.
  • Try it. Sign in as one user, then ask the API for another user’s data with the public key, outside the app. If it answers, so will it for anyone.
  • Hiding a button is not access control. An admin page needs a check on the server or in the database, not only in the interface.
  • The service_role key skips every rule. It lives on the server only.

4. Sign-in on the real domain

Sign-in that works in the preview and loops or fails live is almost always a list of allowed URLs.

  • Your auth provider has a site URL and a list of allowed redirect URLs (in Supabase: Authentication, URL Configuration). Add the production domain, with and without www if you use both.
  • Google, GitHub and other sign-in providers keep their own list of callback URLs, in their own console. Add production there too.
  • Confirmation and password-reset emails: the built-in mailer (Supabase’s, for one) is meant for testing and sends only a few emails an hour. Connect a real email provider before launch, and check that the links in the emails point at production, not the preview.

5. The database itself

  • Is production a separate database from the one you tested with? If the preview and the live app share one, every test writes into your customers’ data.
  • Schema changes belong in migration files in the repository, not in clicks in a dashboard. Otherwise nobody can rebuild the database, and the next AI change guesses at a schema it can’t see.
  • Backups: check that your plan includes them, then restore one into a scratch project once. Until you’ve done that, you don’t know the backups work.

6. The build and the host

  • Run the production build on your machine (npm run build, then npm run preview or npm start). Development servers forgive type errors and missing files that a build doesn’t.
  • Search the code for localhost and the preview’s own address. Both still turn up in API calls and redirects.
  • File names: macOS and Windows ignore case, Linux servers don’t. import Header from './header' works on your laptop and fails on the host if the file is Header.tsx.
  • A single-page app needs a rule that sends every unknown path to index.html. Without it, reloading any page except the home page gives a 404.

7. Limits and costs

  • Rate-limit sign-up, sign-in and anything that calls a paid API. An AI feature with no limit is a bill anyone on the internet can run up.
  • Set spending caps or budget alerts with the host, the database and every AI provider, before launch.
  • Estimate the monthly cost at a hundred and at a thousand users.

8. Knowing when it breaks

  • Error tracking in the browser and on the server (Sentry or similar), so you hear about errors before your users write to you.
  • An uptime check on the home page and on one page that reads from the database.
  • A staging copy of the app, with its own database, where each change goes first. Deploy there, click through the main flows, then promote.

After the list

None of this is advanced, and most of it is an afternoon’s work. It gets skipped because the AI tools are built to make the preview work, and the preview never asks these questions.

If you’ve been through the list and you’re still stuck, that is the work I do: getting AI-built apps to production. The first call is free.