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 Lova
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.envandimport.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_andsk_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_roleand 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
.envfile 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;gitleaksruns 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_rolekey 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
wwwif 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, thennpm run previewornpm start). Development servers forgive type errors and missing files that a build doesnβt. - Search the code for
localhostand 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 isHeader.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.
First published at peterlada.com.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.