Guides · Updated 2026-10-22
Your Lovable app works in preview and breaks after you publish
Why a Lovable app can work in preview and fail on the published URL, and how to check env vars, Supabase redirect URLs, RLS, API keys, and the production build.
Preview and the published site are not the same app
Lovable's preview is a convenient place to click around while you build. The published URL is a different deployment. It has its own origin, its own build, and its own copy of the environment variables that existed at build time. A button that works inside the preview can fail for a stranger who opens the link you posted, and the preview will keep looking fine while you hunt for the bug.
Treat the published URL as the only version that counts. Open it in a private window, on a machine that is not logged into the preview. If signup, login, or a data page fails there, the rest of this page is the order worth checking. None of these checks require you to rewrite the app first.
Environment variables that exist in preview and vanish after publish
Preview often has the project's secrets already filled in. Publish uses whatever was saved for the production deployment, and a client-side framework only ships variables you explicitly marked as public. In a Vite app that prefix is VITE_. In Next.js it is NEXT_PUBLIC_. A variable named SUPABASE_URL with no prefix is available to a server, and it is undefined in the browser. If the preview was reading it through a loophole and the production bundle inlines an empty string, every request goes to the wrong place or nowhere.
Check the published site, not the Lovable settings screen. In the browser's network panel, look at the host the app calls. An empty host, a request to localhost, or a request to the preview origin means the public variable was missing when that build ran. Add the variable, then publish again. Changing the value after the build does not update JavaScript that already baked the old value in.
Do not fix a missing browser variable by marking the service role key, a database password, or a secret API key as public. If the browser needs to talk to Supabase, it needs the project URL and the anon key. Those two are designed to be public. Everything else stays on a server.
Supabase Site URL and redirect URLs
Supabase Auth builds confirmation links, password reset links, and OAuth returns from the Site URL and the redirect allow list. Both live under Authentication, then URL configuration. If Site URL is still http://localhost:3000, or it is still the Lovable preview host, the email your customer receives sends them to a place that is not the site you published.
Set Site URL to the origin people actually use, including https, with no path and no trailing ambiguity you did not intend. Add that same origin to Redirect URLs. A wildcard such as https://your-app.example/** covers the routes your app asks to return to. If the app passes redirectTo, that exact URL has to match an entry on the list. An unlisted redirect is dropped or rejected, which looks like 'the email never came' or 'the link opens and nothing happens'.
Add the preview origin to the allow list only if you still want preview logins to work. Do not leave it as the Site URL once customers are using the published domain. After you change it, request a fresh signup or reset from the published site. Old emails still contain the old link.
Row Level Security
Preview can look generous. A builder session, a disabled RLS switch, or a client created with the service role key will read and write every row. The published app, if it is wired correctly, uses the anon key. With Row Level Security off, that anon key can read the table through the public API. With Row Level Security on and no policy, the same key sees nothing. The page loads, the request returns 200, and the list is empty. That feels like a publish bug. It is the database doing what you asked, or not doing it.
In the Supabase table editor, open each table that holds user data and confirm RLS is enabled. Then read the policies. A policy for the authenticated role does not apply to a visitor who is not signed in. A policy that checks auth.uid() = user_id returns no rows when the session never arrived, which loops back to the redirect URL problem above. Test with the anon key, or by signing in on the published site. Do not test with the service role key. That key bypasses RLS, so a successful query with it proves nothing about the published app.
WhoopsBot's free check does not test RLS. It does not log in, and it does not query your tables. If you want a regression on 'a new user can see their own row', write that as a flow with a test account. The free check can still tell you if the service role key was shipped to the browser, which is the usual reason preview and production disagree this sharply.
API keys
Two keys get confused. The anon key is a long JWT meant for the browser, and RLS is what limits it. The service_role key is a different JWT that bypasses RLS. It belongs in a server environment, an edge function, or a background job. If the published bundle contains it, anyone who loads the page can take it and read the database regardless of your policies.
Search the published JavaScript for service_role. Also compare the Supabase URL in that bundle with the project you think you deployed. A preview project and a production project are easy to swap when both keys sit in the same settings form. After you remove a leaked key, rotate it in the Supabase dashboard. Deleting the line from the source does not invalidate a key that was already downloaded.
The production build is a different program
Preview is often a development server. Publish runs a production build. The production build inlines environment variables, minifies, and drops code it thinks is unused. A module that touched window or document at import time can throw during that build, or throw only in the browser after hydration, while the dev server never noticed. Asset URLs can change. A font or image that resolved in preview 404s after publish, and the console is the only place that says so.
Open the published page, then the console, then the network panel filtered to failed requests. A 404 on a script means the HTML and the files drifted apart. A failed request to your API with a CORS error means the API allow list still names the preview origin. A React error boundary that shows a blank main area with a 200 status is still a broken page. Uptime checks that only look for HTTP 200 will stay green.
How to hear about it next time
Paste the published URL into the free check. It opens that one page and reports whether it loaded, console errors, broken images and same-site links, a few obvious tokens in the JavaScript, whether /.env or /.git answered with a real file, and the usual security headers. It does not click, sign up, or submit a form.
For the path a customer takes, save a flow. Describe the steps in plain language: open the signup URL, create an account with a test inbox, expect the sentence your welcome screen shows. The free plan runs one flow once a day and keeps the result on the run page. Starter runs five flows every six hours and can email you when one starts failing. Pro runs twenty flows every hour and emails on every failure. Either way the failure is a sentence, plus a prompt you can paste into Cursor or Lovable, instead of a green checkmark on a URL that only returned HTML.
Check the published page.
No card needed. Free plan forever. Cancel anytime.