Works in preview, broken when published? It's almost always env vars.
Your Lovable app runs perfectly in the editor preview, then falls over the moment it's live on your real domain. Nine times out of ten the cause is the same: an environment variable or secret that exists in preview but was never set in production. Here's a calm, ordered checklist to find which one — and the exact fix.
Preview and your published site are different environments and don't share secrets automatically. If an edge function 500s or data is blank on live, a server secret is missing in production (#1). If the browser logs an undefined key, a public build-time var is missing or you didn't re-publish (#2, #3). If payments hit test mode on a live site, you're on test keys (#4). Custom domain acting differently? Its env may not match (#5). And check for plain typos in variable names (#6).
First, how to tell this is your problem
The signature of an env-var problem is that the same code behaves differently depending on where it runs. Open your browser's developer console (right-click → Inspect → Console) on the live published URL and look for these tells:
- It works in the editor/preview, fails on the live URL → the two environments have different variables. This is the whole pattern.
- Console shows
undefinedfor a key (e.g. a Supabase URL or key logs asundefined) → a public build-time variable is missing or wasn't rebuilt. See #2, #3. - 500 errors from your edge functions → a server-side secret the function needs isn't set in production. See #1.
- Pages load but data is blank → the app can't reach Supabase or the function bailed out — again, a missing key. See #1, #2.
- Payments use test mode / test cards on a live site → production is still on test keys. See #4.
1. The secret is set in preview but never added to production
This is the most common cause by far. While you were building, you (or Lovable) added a secret — a Stripe secret key, an API key, a Supabase service role key — and it worked in the editor. But Supabase Edge Function secrets live in Supabase, not in your Lovable preview, and they have to be set there explicitly for the deployed function to see them.
Fix: In Supabase → Project Settings → Edge Functions → Secrets, confirm every secret your functions use is present with the correct value. Crucially, re-deploy the function after adding or changing a secret — Edge Function secrets are baked in at deploy time, so a function deployed before you set the secret won't pick it up until it's redeployed. After redeploying, retry the action and watch Supabase → Logs → Edge Functions for the error to clear.
X is not defined or missing environment variable points straight at an unset secret — not a bug in your code.2. Client-side vs server-side variables (this trips everyone up)
There are two completely different kinds of variable, and mixing them up causes both broken pages and dangerous leaks.
Public, build-time variables are anything the browser itself needs — your Supabase project URL and the anon/public key, your Stripe publishable key. For a Vite app (which Lovable builds), these must carry a VITE_ prefix so they get compiled into the front-end bundle. They are public by design: anyone can read them in your shipped JavaScript, and that's fine, because they're not secret.
Server-only secrets are the powerful keys — a Stripe secret key, a Supabase service role key, any private API secret. These must live only in server-side code (your Supabase Edge Functions) and must never be VITE_-prefixed or read in the browser.
Fix: Decide for each variable: does the browser need it? If yes, it must be a public build-time var (and must be safe to expose). If it grants real power, it's a server secret — set it as a Supabase Edge Function secret and read it only inside the function. Here's the distinction in code:
// ✅ Public — safe in the browser, VITE_ prefix, compiled into the bundle
const url = import.meta.env.VITE_SUPABASE_URL;
const anonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
// ✅ Server-only secret — read ONLY inside a Supabase Edge Function
// NEVER VITE_-prefixed, never sent to the browser
const stripeSecret = Deno.env.get("STRIPE_SECRET_KEY"); // sk_live_…
sk_…) or a Supabase service role key to the browser. Never give them a VITE_ prefix and never read them in front-end code — every VITE_ variable is compiled into the public bundle that anyone can open and read. If one of these has already shipped to the client, treat it as compromised: roll it immediately in the Stripe or Supabase dashboard.3. You changed a public var but didn't re-publish
Public, build-time variables aren't read live — they're compiled into the JavaScript bundle when the app is built. So if you change a VITE_ value but don't rebuild and re-publish, the deployed site still serves the old bundle with the old value baked in. The change looks correct in your settings yet the live site behaves as if nothing happened.
Fix: After changing any public build-time variable, re-publish the app so a fresh bundle is built with the new value. Then hard-refresh in a private window (to dodge cached files) and confirm the console now logs the new value instead of undefined or the stale one.
Live site down and you can't spot which variable it is?
Send me what's happening and I'll get your published app working again — flat fee, quoted up front, reply the same day. Most env-var rescues are sorted within a day.
Get my app fixed → Flat fee · reply today · no retainer required4. Production is still using test keys (or blank ones)
A close cousin of #1: the variable is set in production, but it's the wrong value — a pk_test_ / sk_test_ Stripe key, or a leftover staging URL, on a site that's now taking real customers. The app "works" but in the wrong mode: test cards pass, real cards don't, or data writes to the wrong project.
Fix: Audit each production variable for which value it holds, not just whether it exists. For a live site, the publishable key should start pk_live_ and the secret key sk_live_. Remember that Stripe's test and live modes have entirely separate products, prices and webhook endpoints — switching to live keys means re-checking those too.
5. Your custom domain doesn't carry the same env as the default URL
Sometimes the app works fine on the default Lovable-provided URL but breaks on your own custom domain. Deployments can be configured per-target, so the custom domain can end up pointing at a build that never received the production environment variables — or received a different set.
Fix: Confirm the deployment serving your custom domain is built from the same configuration and the same environment as the working URL. Set the production variables for the target that actually backs the custom domain, re-publish, then test the live flow on the custom domain specifically — not just the default URL.
6. A typo or wrong name (the variable is there, just misspelled)
Your code reads import.meta.env.VITE_SUPABASE_URL, but the variable was saved as VITE_SUPABASE_URI, or SUPABASE_URL with no prefix, or with a stray space. The name has to match exactly — and for browser vars the VITE_ prefix is part of the name. A mismatch reads back as undefined, which then cascades into blank data or a crash.
Fix: Put the name your code reads side by side with the name in your settings and compare them character for character — prefix, casing, underscores, no trailing spaces. Fix the mismatch, then re-publish (for a public var) or re-deploy the function (for a secret) so the corrected name takes effect.
How to stop it happening again
Env-var breaks are nasty because they're invisible until a real user hits the live site — the preview keeps looking perfect. Two habits prevent almost all of them.
First, a short before-you-publish checklist:
- Every production secret is set in Supabase (Project Settings → Edge Functions → Secrets) and the functions have been re-deployed since.
- Every public
VITE_var is set and you've re-published so it's baked into a fresh bundle. - Live keys, not test keys —
pk_live_/sk_live_— on anything customer-facing. - Names match exactly between your code and your settings, prefix included.
- You tested the live URL and your custom domain in a private window, not just the preview.
Second — because a checklist only catches what you remember to check — put a watch on production that catches a broken deploy for you. That's exactly what Backstop does: it runs your real signup and checkout flows as live browser tests around the clock, on the published site, so the moment a missing variable breaks production it tells you in plain English — within minutes, not when a customer finally emails. It also keeps offsite backups of your Supabase data, so a bad deploy is never the end of the story.
Catch the next break before your customers do.
Join the waitlist for Backstop — monitoring + offsite backup built for Lovable apps. We'll email you at launch, nothing else.
Frequently asked
Why does my Lovable app work in preview but not when published?
The preview and the published site are two different environments, and they don't automatically share the same environment variables and secrets. A key you set while building in the editor often isn't present on the live deployment — so on the published URL the app reads an undefined value, an edge function 500s, or data comes back blank. Set the same variables on production, re-deploy your Supabase Edge Functions so the secrets are baked in, and re-publish so any public build-time vars are rebuilt into the bundle.
Where do I set environment variables for a Lovable + Supabase app?
It depends on whether the variable is used by your server code or your browser code. Server-only secrets for Supabase Edge Functions live in Supabase under Project Settings, Edge Functions, Secrets, and you must re-deploy the function after changing them because secrets are baked in at deploy time. Public build-time variables that the browser needs — like your Supabase URL and anon key, named with a VITE_ prefix for a Vite app — are set in your Lovable project's environment settings and only take effect after you re-publish, because they're compiled into the deployed bundle.
Which env vars are safe to expose to the browser?
Only values that are designed to be public: your Supabase project URL and the anon/public key, your Stripe publishable key (pk_), and similar non-secret identifiers. Anything that grants real power — a Stripe secret key (sk_), a Supabase service role key, or any API secret — must never be exposed to the browser and must never carry a VITE_ prefix, because every VITE_ variable is compiled into the public JavaScript bundle that anyone can read. Keep secrets server-side only, as Supabase Edge Function secrets, and if one ever leaks, roll it immediately.