Nothing you did was wrong
You spent weeks building it. It worked. Every feature you tested did what it was supposed to do, right up until the moment you pressed deploy.
Then: a build that fails with "check logs" and a log that says nothing. Or a deploy that succeeds and serves a blank page. Or users who log in successfully and land straight back on the login screen.
So you changed something and redeployed. Then something else. You've now done that enough times that you're not sure what state the project is in, and each round takes longer than the last.
Your code is almost certainly fine. Nothing in the repository changed between the version that worked and the version that doesn't. What changed is where it's running — and that's a much shorter fix than it feels like right now.
Here's the part nobody tells you.
The preview is designed to make things work. Production isn't.
That sentence is the whole page.
Your builder's preview environment runs with credentials, environment variables, database connections and permissions handed to it automatically. That's exactly what makes building feel fast — you never had to configure any of it, because the preview configured it for you.
Production has only what you explicitly gave it. Every assumption the preview quietly satisfied becomes a failure the moment you deploy.
Two things make this hard to diagnose:
The errors describe the symptom, not the gap. A missing environment variable doesn't say "missing environment variable." It says the build failed, or the page is blank, or the API returned 500. You're given the consequence and left to infer the cause.
Nothing in your code changed, so the natural next move is to prompt about it. That cannot work. Lovable, Bolt, Cursor and Replit can all read your code, and your code is not where the problem is.
So what's actually going on?
What you see, and what's usually behind it
Find your symptom, then read the detail underneath.
What you see | What's usually wrong |
Build fails with a generic message | Missing environment variable at build time, or a dependency not in the lockfile |
Deploys successfully, page is blank | Frontend runtime error the build never caught — check the browser console |
Login succeeds, then bounces to login | Production domain missing from the auth provider's redirect URL list |
"Failed to fetch" on every API call | CORS blocking your own domain, or an API base URL still pointing at localhost |
500 on every backend request | Missing secret, or a database the deployment can't reach |
Nothing saves | Connection string pointing at a development project, or one that was deleted |
Stripe or OpenAI calls fail silently | API key absent from production, error swallowed rather than surfaced |
Hydration mismatch in the console | Server and client rendering different output — dates, randomness or browser-only code |
Images and assets 404 | Paths that resolved relative to the preview and don't on a real host |
Custom domain doesn't load | DNS, SSL provisioning, or the domain never added to the hosting project |
Env var changes have no effect | Value read at build time and baked in — needs a rebuild, not a restart |
Worked yesterday, broken today | Expired credential, floating dependency version, or a provider-side change |
The eight causes underneath
Environment variables that exist in the builder and not in production. The most common cause by a wide margin. Either the build fails outright, or it succeeds and every external call — Supabase, Stripe, OpenAI, SendGrid, Firebase — fails silently at runtime.
Auth redirect URLs pointing at the preview domain. In Supabase this is the Site URL and Redirect URLs list. In Google OAuth it's the authorised redirect URI. In Clerk or Auth0 it's the allowed callback. Login itself works; the session is discarded on the way back.
Database credentials scoped to development only. The app connects to a Postgres, Supabase or Neon project that exists for development, or to nothing at all. Migrations that ran in development never ran in production, so the schema doesn't match what the code expects.
Build-time versus runtime configuration. Anything prefixed NEXT_PUBLIC_, VITE_ or similar is read at build time and baked into the bundle as whatever it was when you deployed. Changing it afterwards changes nothing until you rebuild. This one produces the maddening "I updated it and it didn't help."
CORS and allowed origins never updated. The browser blocks requests from your production domain to your own API. The console says so clearly. The build log says nothing at all, which is why people miss it for hours.
Hardcoded URLs left from development. localhost:3000 in production code. Obvious the moment you see it, invisible until then, and AI builders generate it routinely because it's correct while you're building.
No error boundary. A single runtime failure in one component blanks the entire page rather than that component. This is what most white screens actually are — not a deploy failure, a rendering failure with nothing to catch it.
DNS, SSL and domain configuration. The last step and the most common cause of downtime during a launch, because it's usually left until everything else is done and then rushed.
Before you contact anyone — four of these you can check yourself.
Four checks, in this order
Most cases resolve at one of them, and we'd rather you didn't pay us for that.
1. Compare your environment variables Open your builder's variables list next to your hosting provider's. Not from memory — side by side. Anything present in one and missing from the other is your answer more often than not.
2. Check your auth redirect URLs In Supabase: Authentication → URL Configuration. Your production domain needs to be in both the Site URL and the Redirect URLs list. In Google Cloud Console it's under your OAuth client's authorised redirect URIs. The preview domain being there is not enough.
3. Open the browser console on the live site Press F12, go to Console, reload. Not the build log — the browser console. The real error is almost always sitting there in one line, and it usually names the exact thing that's missing.
4. Confirm the database exists Check that the project your production connection string points at is real, current, and not your development one. Then check whether your migrations actually ran against it.
If those four don't resolve it, something else is going on.
When to stop redeploying
You've been on the same failure for more than two hours. Past that you're not converging on a fix — you're accumulating changes and moving away from a known-good state.
You can't say what's changed since it last worked. That's the point where each new attempt makes diagnosis harder rather than easier.
The app is live and users are affected. Experimentation now has an hourly cost, and the arithmetic has changed.
There's a quieter signal too: you've started making changes you don't understand, because something on the internet suggested them. That's usually the moment an outside reader is the fastest route out.
Here's what that looks like.
We reproduce it in production, not on a laptop
That distinction matters more than it sounds. A local copy is a third environment with its own gaps — fixing something locally tells you almost nothing about why production fails.
An engineer reads the code, the build configuration and both environments side by side, and reproduces the failure where it's actually happening. Once reproduced, the cause is usually unglamorous and quick.
The work that matters is what comes next. If one environment gap exists, others almost always do, and they're sitting there waiting for your next deploy. We find those before they surface, which is the difference between fixing this failure and fixing this class of failure.
You get a working deployment, a written note on what was wrong, and a list of everything else that would have broken next.
What we won't do
Hardcode a value to make the deploy pass. That's how this becomes a worse problem in six weeks.
Fix one variable and leave the rest unchecked.
Rewrite anything that was already working.
One thing worth saying about what this is costing you.
The gap compounds quietly
Every failed deploy is time, and if you're building with a metered AI tool it's also credits — spent on a problem the tool structurally cannot solve, because the fault isn't in the code it can read.
Meanwhile the launch date moves, the customer who was going to see it this week doesn't, and the codebase accumulates changes made under pressure that nobody documented.
None of that is a reason to panic. It is a reason to stop and have someone read it, rather than deploying a fourteenth time.
Which builder you used changes where it bites.
By tool
The gap is universal. Its favourite hiding place isn't.
Tool | Where it usually goes wrong |
Lovable | Supabase connection and auth redirects. The preview works because you're logged in and the redirect list has the preview domain. Production has neither. |
Netlify deploys failing on a missing Supabase anon key, with a build error that names nothing useful. | |
Replit | Secrets set in the workspace and never carried into the deployment, plus host binding to localhost. |
v0 | API routes that work locally and fail in production, and hydration mismatches that only appear once rendered on a server. |
Cursor | Environment configuration assumed rather than checked, because the model had no way to see your hosting setup. |
Base44 | Third-party keys never promoted out of development, and deployment configuration left at defaults. |
How a rescue runs
01 — Audit. You send the repository. An engineer reads the code, the build config and both environments, and comes back within 48 hours with what's broken and what else is waiting. Free.
02 — Reproduce. The failure confirmed in your actual production environment, not assumed from an error message.
03 — Fix. The real cause, delivered as a change you can read.
04 — Sweep. Every other environment gap found and closed before your next deploy finds it for you.
05 — Separate. Staging made genuinely distinct from production, so the next release is testable before it's live.
06 — Handover. What was wrong, what changed, and a deploy process that doesn't do this again. You own it.
Step five is the one that ends the problem permanently. The gap exists because there was nowhere to catch it.
Questions
Which builders does this apply to? All of them. Lovable, Bolt, Replit, v0, Cursor, Base44, Windsurf. Previews exist to make building easy, and that's precisely what creates the gap.
It deployed successfully but the page is blank. Usually a runtime error the build never caught. The browser console will name it in one line; the build log won't mention it at all.
It worked yesterday and I changed nothing. An expired credential, a floating dependency version, or a provider-side change. All three are common, none are your fault, and all are quick to confirm.
Do I need to move off the builder to fix this? No. This is configuration, not architecture. If moving is genuinely right for other reasons, we'll say so separately rather than bundling it into a deploy fix.
Can you set up staging so this stops happening? Yes, and it's usually the most valuable part of the engagement. The gap exists because there's nowhere to catch it before users do.
I changed the environment variable and nothing happened. If it's a build-time variable, the old value is baked into the bundle. It needs a rebuild, not a restart.
Will you sign an NDA? Yes, mutual, before you send anything. One senior engineer reads your code, it's never used for training, and access is removed after delivery.
My app is live and users are affected right now. Tick the box on the form. Live incidents jump the queue.
Send us the repository
You'll get what's actually failing, what else would have failed on the next deploy, and what it takes to make production predictable.
Also broken?
Supabase · Authentication · Lovable · Bolt.new · Replit · The AI keeps breaking working code · Security audit