Bolt.new runs your entire stack inside a browser tab. The dev server, the bundler, the Node runtime, even the "backend" — all of it executes in a WebContainer on your machine. That is why it feels instant, and it is also why the single most common message we get from Bolt users is some version of:
It works in Bolt. I deployed it to Netlify and now it doesn't.
Nothing is wrong with your app. The environment it was built in is not the environment it runs in, and Bolt never had to notice the difference. We have fixed enough of these to know that it is almost always one of five causes, so here they are, in the order to check them.
First, read the actual error
Before anything else, open the deployed site, open the browser console, and reproduce the failure. Then open the Netlify deploy log for the same build. You are looking for one of these shapes:
| What you see | Which cause below |
|---|---|
undefined where a config value should be, or Cannot read properties of undefined | 1 — environment variables |
A request to /.netlify/functions/... returns 404 or 502 | 2 — the function was never deployed |
blocked by CORS policy in the console | 3 — CORS |
| Works logged out, breaks logged in, or the reverse | 4 — Supabase and auth |
| The build itself fails, or deploys a blank page | 5 — the build step |
If none of them match, skip to the end — it may be the sixth thing.
1. The environment variable exists in Bolt and nowhere else
In the WebContainer, Bolt keeps your .env values in memory and injects them
into the dev server. On Netlify, nothing is injected unless you put it there
yourself. The code is fine; the value is undefined.
Check the Netlify dashboard under Site configuration → Environment variables
and compare it against the .env in your project. Every variable the app reads
has to be there, spelled identically.
Two subtleties catch people out:
- Prefixes control exposure. In a Vite project, only variables prefixed
VITE_are bundled into the client. If your code readsimport.meta.env.API_URLit will always beundefinedin the browser; rename it toVITE_API_URL. The reverse is worse — see cause 4. - Netlify Functions read
process.env, notimport.meta.env. A variable set in the dashboard is available to functions at runtime, but the client only gets it if it was present at build time with the right prefix.
2. The function ran in preview but was never deployed
Bolt will happily write a netlify/functions/send-email.ts and call it from
the client, and in the WebContainer the call works because Bolt simulates the
function locally. On Netlify, the function only exists if the build picked it up.
Check the deploy log for a line listing the functions that were bundled. If yours is not there:
-
The directory is wrong. Netlify looks in
netlify/functionsby default; if Bolt put them infunctions/orsrc/functions/, either move them or set the path innetlify.toml:[functions] directory = "netlify/functions" -
The function imports something that is not in
package.json. Bolt sometimes uses a dependency without adding it. The local simulation forgives this; the bundler does not. -
The file is TypeScript with a syntax the bundler rejects. The log will say so.
The same applies if Bolt used Supabase Edge Functions instead: they run in preview, but they have to be deployed to your Supabase project separately, which Bolt's publish button does not do.
3. CORS was open in preview and is not in production
In the WebContainer, the client and the "server" share an origin, so CORS never
comes up. Deployed, your client is on your-site.netlify.app and it is calling
something else — a third-party API, a Supabase function, a backend on another
host — and that something is refusing the origin.
Read the error carefully. It names the origin that was blocked and the header that was missing. Then fix it at the right layer:
- Your own function should set
Access-Control-Allow-Originto your domain, not*. Bolt tends to generate*, which "works" and also means any site on the internet can call your function with your users' cookies. - A third-party API that blocks browsers is telling you it should be called from a server. Move the call into a Netlify Function and call that instead. This is also where the key goes (see cause 1).
4. Supabase behaves differently for a real user
This is the one that looks like a random bug. Everything works when you test logged out, or it works when you are logged in, and breaks for a second account.
The cause is Row Level Security. In preview you were probably testing as the
same user who created every row, or with RLS not yet enabled, so every query
returned everything. In production a different user hits a policy — or the
absence of one — and gets an empty result or a permission denied.
Check every table:
select tablename, rowsecurity as rls_enabled
from pg_tables
where schemaname = 'public'
order by rls_enabled, tablename;A table with RLS off is readable and writable by anyone with your public anon key, which is everyone. A table with RLS on and no policy returns nothing. What you want is RLS on with a policy per operation, and the five RLS mistakes we find in almost every AI-generated app walks through the exact SQL.
5. The build step Bolt never ran
In the WebContainer you were running a dev server the whole time. A production
build — vite build — is a different process with stricter rules, and Bolt may
never have executed it once.
Read the deploy log from the top. The usual culprits:
- TypeScript errors. The dev server tolerates them; the build does not. Fix them or, as a temporary measure, exclude the type check from the build command — and put it back before you call this done.
- Case-sensitive imports.
import Button from './button'findsButton.tsxon a Mac and fails on Netlify's Linux builders. - A build command or publish directory that doesn't match the framework. For
a Vite project it should be
npm run buildanddist. Bolt sometimes writesbuildor leaves the field empty. - Node version. Set it explicitly in
netlify.tomlor with a.nvmrc, so the builder uses the version your dependencies expect.
The sixth thing: it worked, and then it stopped
If the app deployed fine and broke later without a code change, look at whatever changed underneath it. The most common: a Supabase project paused for inactivity on the free tier, an API key rotated or hitting its quota, or a domain's certificate failing to renew. None of these are Bolt's fault either, and all of them are things a working app needs someone watching for — monitoring and alerts is how you stop finding out from customers.
Making it not happen again
Every one of the five is a symptom of one gap: the app has never been built and run outside the editor in a controlled way. The permanent fix is small and mechanical:
- Export to GitHub and connect Netlify to the repo instead of publishing from Bolt.
- Add a
netlify.tomlthat states the build command, publish directory, functions directory and Node version explicitly. - Turn on deploy previews, so every pull request gets its own URL and you see the production build before it replaces the live site.
- Put every environment variable in the dashboard, with the secrets in
functions and only the genuinely public values under
VITE_.
You can keep working in Bolt after this; it pulls from the same repo. What changes is that "it works in Bolt" stops being the last check before customers see it.
If you would rather have someone go through your Bolt project and hand you the list, the free launch audit is exactly that — a senior engineer, your repo, a ranked report within 48 hours, and a fixed price against each fix. What we fix in Bolt.new apps covers the rest of what we find.


