TL;DR: In a Bolt.new app, every var that starts with VITE_ is baked into the client bundle at build time. That is by design — the prefix marks a value as safe for the browser. The trouble starts when the AI adds that prefix to a secret key just to make the app build. The fix: sort your keys into public and private, move private ones behind edge functions, and check your own bundle before launch. Here is how.
Bolt.new grew faster than almost any tool in the space — around $20M ARR in about two months, $40M in about five, and roughly five million users in year one. That is a lot of shipped apps. Most of them run on Vite. And Vite has one rule founders keep learning the hard way: VITE_ means public.
Want the fast answer for your own app? Run the free scan on your live URL. It checks for exposed keys with no code access, in about 30 seconds. Then read on.
VITE_ variables secret?service_role key in the bundle. It bypasses RLS. Rotate it.How env vars work in a Bolt.new app
Bolt.new projects are Vite projects. Your vars live in a .env file. At build time, Vite reads them and injects values wherever the code says import.meta.env.SOMETHING.
Vite splits those vars into two groups with one rule:
# stays on the server side of the build
STRIPE_SECRET_KEY=sk_live_...
# shipped to every visitor's browser
VITE_SUPABASE_URL=https://xyz.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbG...
Anything with the VITE_ prefix gets built into the frontend bundle. Anything without it cannot be read by frontend code at all. That is the whole system. It is simple, and it is honest — the prefix is a public label.
Why the prefix ships to the browser by design
This is not Bolt.new being careless. Frontend code runs on your visitor's machine. If the code needs a value, the value must travel to that machine. Vite just makes you say so, out loud, with a prefix.
The trap is how AI builders use it. The model needs a key to make a feature work. The build fails because frontend code cannot read an unprefixed var. The shortest fix — the one the model reaches for — is to rename the var with VITE_. The build passes. The demo works. And the secret is now inside a JS file sent to every visitor.
We have seen secret payment keys, service_role tokens, and admin API keys shipped exactly this way. Not because anyone chose to share them. Because the prefix made the error go away.
Treat any
VITE_variable as public the moment you type it. If the value would hurt you on a billboard, it does not get the prefix.
Public keys vs private keys: the sorting table
Every key in your project belongs to one of two buckets. Sort them once, before launch.
| Key | Bucket | Why |
|---|---|---|
| Supabase anon key | Public — VITE_ is fine | Built to be public, but only if RLS is on and policies are scoped |
Publishable payment key (pk_live) | Public | Made for the browser |
| Stats or analytics ID | Public | No write power |
Secret payment key (sk_live) | Private — server only | Can charge and refund. Never in the bundle |
Supabase service_role key | Private — server only | Bypasses RLS. Full database access |
| Database connection string | Private — server only | Direct access to everything |
| JWT signing secret | Private — server only | Whoever holds it can mint sessions |
| Third-party API keys with write access | Private — server only | Spend and abuse happen on your account |
Not sure which Supabase key is which? Read anon key vs service role key — it is the single most costly mix-up we see in Bolt-built apps.
Moving secrets to edge functions: the real fix
A secret that your app logic needs, but the browser must never see, belongs behind a server boundary. In the Bolt.new stack that usually means a Supabase Edge Function or a small server route. The pattern is the same either way:
- Drop the
VITE_prefix so Vite stops bundling the key. - Store the key as a server-only var in your host or edge function settings — not in a file that deploys with the frontend.
- Write a small edge function that reads the key, calls the outside API, and returns only the data the client needs.
- Point the frontend at your own endpoint instead of the third-party service.
The shape of the function is short:
// supabase/functions/quote/index.ts
Deno.serve(async (req) => {
const key = Deno.env.get("PRICING_API_KEY"); // server-only
const res = await fetch("https://api.vendor.com/quote", {
headers: { Authorization: `Bearer ${key}` },
});
const data = await res.json();
return Response.json({ price: data.price }); // only what the client needs
});
One more step people skip: add auth and a rate limit to that endpoint. An open proxy with your key behind it still spends your money. Our rate limit checker tests one endpoint free.
Check your own bundle in two minutes
You do not need an engineer for the first pass.
- Open your live app. Open dev tools (F12, or right-click and Inspect).
- In the Sources or Network tab, find the built JS file. It is usually named like
index-a1b2c3.js. - Search inside it for
sk_live,service_role,VITE_, and the first chunk of any key you use. - If a private key shows up, it is public. Rotate it today, then move it server-side.
The hand check catches the plain cases. The free scan goes further: it reads your live bundle, probes for exposed config files, and checks your security headers in the same pass. Our guide on finding exposed secrets in AI bundles shows the full manual method.
The service_role blind spot
A leaked env var and broken Row Level Security are the same failure wearing two hats. Your RLS policies can be perfect. If the service_role key sits in the public bundle, none of them matter — that key bypasses RLS by design. Whoever finds it does not need to break your policies. They hold the master key.
This is the exact pattern our scanner flags in the wild. As pfs.js:289 puts it: "AI-generated apps frequently ship with Supabase anon key exposed and no RLS. Run an authorized Deep Audit to verify." The outside data agrees. Symbiotic Security scanned 1,072 AI-built apps: 98% had at least one flaw, and 16% had critical ones. Our free Supabase RLS checker tests your live URL for both halves — RLS coverage and key exposure — in about a minute.
One legal note
A leaked database key is not just an engineering slip. GDPR expects you to guard personal data with sound technical steps. A key in a public bundle that lets anyone read your users table is the opposite of that. And if your app has an AI feature and EU users, the EU AI Act's Article 50 transparency rules apply from August 2, 2026. Fines for that breach run up to €15 million or 3% of turnover. What counts is where your users live, not where your company sits. Our Compliance Score runs 60 checks across both, for $799.
What the full check costs
The free scan is $0: a Launch Readiness Score out of 100 for your live URL in about 30 seconds. The full Launch Readiness Audit is $499 one time — a branded report, a ranked fix plan with time estimates, and a benchmark against 200+ audited apps. Daily scans start at $149/mo. The full Bolt playbook lives in our Bolt security audit guide and on the Bolt security audit page.
FAQ
Are Bolt.new environment variables safe by default?
No. Any var that starts with VITE_ is baked into the client JavaScript at build time. Anyone who opens dev tools can read it. Only unprefixed, server-side variables stay out of the browser.
Why does Vite put VITE_ variables in the browser?
By design. The prefix is how Vite decides what frontend code may read. It is not a bug and not Bolt.new being careless. The risk comes when an AI builder adds the prefix to a private key just to make the app build.
How do I move a secret to an edge function in a Bolt.new app?
Drop the VITE_ prefix so the key stays out of the bundle. Store it as a server-only variable. Write an edge function that reads the key, calls the outside API, and returns only the data the client needs. Point the frontend at your own endpoint.
How can I check my own bundle for leaked keys?
Open your live app, open dev tools, and find the built JS file. Search it for sk_live, service_role, and your other key prefixes. If a key shows up, it is public. A URL-based free scan runs the same check plus many more.
What if my Supabase service_role key was exposed?
Rotate it now. That key bypasses Row Level Security by design, so anyone who found it could read every table. After rotating, move it behind an edge function and re-check the bundle before you ship again.
Which keys are safe to expose in a Bolt.new app?
Keys built to be public: the Supabase anon key with RLS on, browser-safe payment keys, and stats IDs. Everything else — secret keys, service_role, database URLs, signing secrets — must stay server-side.
The pattern here is the one that runs through every AI-built stack: the code works long before it is safe. Bolt.new wired the feature. The prefix shipped the key. Sorting your env vars is a one-afternoon job — and the scan tells you if it is already too late for one of them.
Is a key sitting in your bundle right now?
Scan your live app free. Exposed keys, headers, and a Launch Readiness Score out of 100. About 30 seconds. No code access.
Run the free scan