Is my Bolt app secure?
Bolt is very good at getting you from an idea to a deployed app without stopping. The problem is that the thing it's best at โ never making you think about the boundary between your machine and the internet โ is exactly the thing security depends on.
Most of what goes wrong in a Bolt app traces back to one misunderstanding, and it's a misunderstanding Bolt's own preview quietly encourages.
1. The preview lies to you about what's private
While you're building, Bolt runs your whole project inside a WebContainer โ a Node environment running in your browser tab. It's a genuinely clever trick, and it means your "server" code and your frontend code are both executing in the same place, on your own machine, where nobody else can see either of them.
So during development, putting an API key in a file feels fine. It works, nothing warns you, and the key is genuinely private โ because it's on your computer.
Then you deploy. The parts that were "server" code during preview don't automatically become a server. If your app deploys as a static site, there is no backend at all โ every file that ends up in the build is downloadable, and any key inside it is public.
The question worth asking about any secret in a Bolt project is not "is this in a config file?" but "after the build, does this end up in something a browser downloads?" If yes, it's published.
2. VITE_ is a publishing instruction
Bolt projects are usually Vite-based, and Vite has a rule that catches
almost everyone: only variables prefixed VITE_ are visible to
your app code. So when a variable seems to be missing at runtime, the
obvious fix is to add the prefix โ and it works immediately.
What actually happened is that Vite substituted the value directly into your JavaScript bundle at build time. The prefix isn't a visibility setting, it's a decision to ship the value to every visitor.
VITE_SUPABASE_ANON_KEYโ fine, that key is designed to be public.VITE_OPENAI_API_KEYโ your OpenAI billing is now public.VITE_STRIPE_SECRET_KEYโ anyone can charge and refund on your account.VITE_SUPABASE_SERVICE_ROLE_KEYโ full read/write on your database, bypassing every rule you set.
Grep your project for VITE_ and read every result. If the
name contains SECRET, PRIVATE, SERVICE_ROLE, or the name of something you pay
for, that value needs to move server-side and then be rotated.
3. Calling AI and payment APIs from the browser
A Bolt app often has no backend at all, so when you ask for a chatbot the
straightforward thing is a fetch to OpenAI or Anthropic directly
from a component. It works in the preview. It works in production too โ for
you and for whoever finds the key.
If you see dangerouslyAllowBrowser: true, that's the SDK
telling you it noticed. It's the one place the tooling actually warns you,
and it's phrased as a flag you can switch off rather than a mistake.
The fix is a small serverless function that holds the key and forwards the request โ a Netlify or Vercel function, or a Supabase edge function if you're already using Supabase. Add a rate limit while you're there: an open AI proxy is free credits with your name on the invoice.
My app calls the OpenAI API directly from the browser. Create a serverless function that makes the call using a key from a server-side environment variable, change the frontend to call that function instead, remove dangerouslyAllowBrowser, and add basic rate limiting to the function.
4. Where your environment variables actually live after deploy
A .env file in your project is a local development
convenience. Your host does not read it โ you set environment variables in
the host's own dashboard, and if you skip that step the app breaks in
production in a way that tempts people into hardcoding the value "just to
get it working".
Two things to check:
- Is
.envin.gitignore? If you've pushed the project to GitHub, this is the difference between a private file and a published one. Public repos are scraped for keys within minutes of a push. - Is
.envreachable on the live site? Openyoursite.com/.env. It should not return a file. If it does, every credential in it is public right now and needs rotating, not just deleting.
5. If you added a database
Bolt apps commonly use Supabase. If yours does, your browser talks to the database directly using a public key, and the only protection is Row Level Security. Off, or set to allow everything, means anyone can read and write every row โ not just their own.
That deserves more space than a paragraph: the RLS section of the Lovable guide covers it properly, and applies identically here.
Before you ship
- Grep for
VITE_; nothing secret carries the prefix - No API keys in any file that ends up in the build
- AI and payment calls go through a serverless function, rate limited
- Environment variables set in your host's dashboard, not hardcoded
.envin.gitignore, andyoursite.com/.envreturns nothing- Row Level Security on, if you're using Supabase
- Every key that was ever exposed rotated at the provider
Bolt changes quickly, and the specifics of its deploy flow may have moved since this was written. The check that doesn't go stale: build your app, then search the build output for anything that looks like a key. Whatever is in there is public.
Check it in about ten seconds
vibecheck scans your project folder or your live URL for everything on this page and explains what it finds in plain English โ with a fix prompt you paste straight back into Bolt.
Scan my app โ free