Lovable app works in preview but breaks after deploy: 6 fixes
You built the app in Lovable, Bolt or Cursor. In the preview everything works: you can log in, data loads, every page opens. Then you publish it, or push it to Vercel or Netlify, and something is wrong. You might see a blank white page, a login button that does nothing, a dashboard that loads with no data, or a 404 when you refresh a page.
This is the most common complaint we see from people building with AI tools, and it is almost never "the AI wrote bad code". The preview gives your app things the live site doesn't have: injected keys, an approved domain, a dev server that forgives routing mistakes, and a proxy that hides cross-origin problems. Once you publish, all of that goes away.
This guide covers runtime failures, where the deploy succeeds but the live app misbehaves. If the build itself goes red, start with the build section below or Vercel Module not found.
When the build itself is red (not just the live site)
This guide is mostly about runtime failures after a green deploy. If Vercel or Netlify shows a red build log, fix the build first. The usual causes from Lovable exports are:
- TypeScript errors the preview ignored — run
npm run buildlocally; fix realstring | undefinedcases instead ofas any. - Case-sensitive imports — macOS allows
./Components/Buttonvs./components/button; Linux CI does not. Match file names exactly. - Missing or mismatched lockfile — keep one of
package-lock.json/pnpm-lock.yaml/bun.lockb, commit it, set the install command. - Node version drift — pin
"engines": {"node": "20.x"}inpackage.json. - Module not found — see Vercel build failed: Module not found.
Once npm run build is green locally and on the host, come back here for blank pages, auth redirects and empty data.
Step 0: read the real error (2 minutes)
Don't ask the AI to "fix the deploy" yet. Open the live site in Chrome, press F12 (or Cmd+Option+I on a Mac), and reload the page.
- Console tab: red lines such as
Uncaught Error: supabaseUrl is required,Failed to fetch, orBlocked by CORS policy. - Network tab: filter by Fetch/XHR. Look for requests with status 401, 403, 404 or 500, or for requests that go to
localhostorundefined.
Copy the first red error exactly. Nearly every cause below leaves a recognisable signature, and the table in each section tells you which one you're looking at.
1. Blank white page: missing environment variables
Signature: the page is completely white, and the console shows something like supabaseUrl is required, Invalid URL, or Cannot read properties of undefined.
In the Lovable or Bolt preview, the platform injects your Supabase URL and anon key for you. Your host knows nothing about them. Lovable and Bolt projects are Vite apps, and Vite only exposes variables whose names start with VITE_. It also bakes their values into the JavaScript at build time. So a variable you add after the build won't appear until you rebuild.
VITE_SUPABASE_URL=https://abcdefgh.supabase.co VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...
- Find the exact variable names your code uses: search the project for
import.meta.env. - Add each one in your host's settings (Vercel: Project → Settings → Environment Variables; Netlify: Site configuration → Environment variables). Tick Production and Preview.
- Trigger a new deploy. Re-running an old build won't pick up new values.
A white screen with no useful message often means the Supabase client crashed while the module loaded. A small guard makes the failure obvious next time:
// src/integrations/supabase/client.ts
const url = import.meta.env.VITE_SUPABASE_URL;
const key = import.meta.env.VITE_SUPABASE_ANON_KEY;
if (!url || !key) {
throw new Error("Missing VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY at build time");
}
export const supabase = createClient(url, key);
Some Lovable projects hard-code the URL and anon key straight into client.ts. In that case env vars aren't your problem, so move on to the next section. The anon key is meant to be public. The service_role key is not, and it must never appear in frontend code.
2. Login does nothing or bounces to localhost
Signature: after you sign in, or click the magic link, you land on http://localhost:3000 or localhost:5173, or you end up back on the login page.
Supabase only redirects to URLs you have approved. The preview domain was approved for you, but your new domain isn't. Fix it in three places:
- Supabase → Authentication → URL Configuration. Set Site URL to your live origin, for example
https://myapp.com. Under Redirect URLs, addhttps://myapp.com/**, and keep your preview URL and localhost too if you still use them. - Google Cloud Console (if you use Google sign-in): the OAuth client's authorised redirect URI should be your Supabase callback,
https://<project>.supabase.co/auth/v1/callback, and your live domain should be listed as an authorised JavaScript origin. - Your code. Search for
redirectTo,emailRedirectTo, andlocalhost:
await supabase.auth.signInWithOAuth({
provider: "google",
options: { redirectTo: `${window.location.origin}/auth/callback` },
});
Test in an incognito window, because old sessions and cached OAuth state hide whether the fix worked. We go deeper on this in Supabase auth broken in a Bolt or Lovable app.
3. Refreshing a page gives a 404
Signature: the home page works and clicking around works, but reloading /dashboard or opening a shared link shows the host's 404 page.
A React single-page app has exactly one real file, index.html. React Router handles /dashboard in the browser. When you reload, the server looks for a file called /dashboard, can't find one, and returns a 404. The dev server rewrites these requests for you, but production hosts don't unless you tell them to.
// vercel.json (project root)
{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }
# Netlify: public/_redirects
/* /index.html 200
# Apache / cPanel hosting: .htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.html [L]
Next.js apps don't need this. If a Next.js route 404s, the page file is missing or named wrong.
4. Data never loads: CORS and API URLs
Signature: the console says has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header, or the Network tab shows calls going to http://localhost:3001/api/....
Two separate mistakes look the same:
- A hard-coded dev URL. The AI wrote
fetch("http://localhost:3001/api/items"). That worked on its machine and points at nothing in production. Use a relative path such as/api/items, or an env var likeVITE_API_URL. - A backend that only trusts the preview origin. Your Express server, edge function or third-party API allows the preview domain but not your live one. Add the production origin to its allow-list:
// Supabase Edge Function
const cors = {
"Access-Control-Allow-Origin": "https://myapp.com",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
if (req.method === "OPTIONS") return new Response("ok", { headers: cors });
Restricted API keys fall into the same family. Google Maps, reCAPTCHA and similar keys are often locked to the preview domain, so add your real domain in each provider's console.
5. Logged in, but every list is empty
Signature: no errors at all, and queries return []. Or you see new row violates row-level security policy when you save.
Supabase Row Level Security (RLS) is working as designed here. Once RLS is on, a table with no policies returns nothing to the browser. In the preview you may have been using seeded data, a different project, or a policy the AI later removed. Add a policy for each operation, keyed on the signed-in user:
alter table public.projects enable row level security; create policy "own rows: read" on public.projects for select to authenticated using (auth.uid() = user_id); create policy "own rows: insert" on public.projects for insert to authenticated with check (auth.uid() = user_id);
Don't "fix" this by turning RLS off. That makes every row readable to anyone holding the public anon key, which means anyone who opens your site.
6. It works for you but not for your users
If your own browser works but friends report problems, suspect stale state. You are still signed in from the preview, or a service worker cached an old bundle. Test in incognito, on a phone over mobile data, and with a brand-new account. Also check the free-tier limits on Supabase auth emails: the built-in email sender is heavily rate-limited, so signups can fail quietly until you set up custom SMTP.
A post-publish checklist
- The live site's console has no red errors on load.
- Every
VITE_/NEXT_PUBLIC_variable is set on the host, and you redeployed afterwards. - Supabase Site URL and Redirect URLs include the live domain.
- OAuth providers list the live domain.
- Refreshing a deep link returns the page, not a 404.
- No
localhoststrings are left insrc/(grep -rn localhost src). - Every table has RLS on, with policies for the operations it needs.
- Sign-up, login, create, edit and delete all tested in incognito on the live URL.
How to prompt your AI tool so it doesn't make things worse
"It doesn't work after deploy" invites the model to guess, and guessing is how working code gets rewritten. Give it the evidence and set limits:
The live site at https://myapp.com shows this console error on load: Uncaught Error: supabaseUrl is required. (client.ts:6) It works in preview. Do NOT change UI or refactor. First explain the cause in 2 sentences, then propose the smallest fix.
If the answer is a hosting setting rather than code, as with causes 1, 2 and 3, no code change will help, and a model that keeps editing files is making things worse.
FAQ
Why does my Lovable app show a blank white page after publishing?
Usually the Supabase URL or anon key is missing in the production build. Vite only exposes VITE_ variables and inlines them at build time, so add them on your host and trigger a fresh deploy. The browser console will show an error such as "supabaseUrl is required".
Why does Supabase login redirect to localhost after deploy?
Your production domain is not in Supabase Authentication → URL Configuration (Site URL and Redirect URLs), or the code hard-codes a localhost redirectTo. Add the domain, use window.location.origin for redirects, and test in an incognito window.
Why do I get a 404 when I refresh a page on my deployed React app?
Single-page apps need the host to rewrite every route to index.html. Add a rewrite in vercel.json, a _redirects file on Netlify, or an .htaccess rule on Apache hosting.
Should I disable Row Level Security if my data does not load?
No. Disabling RLS exposes every row to anyone with your public anon key. Add select, insert, update and delete policies scoped to auth.uid() instead.