Are VITE_ environment variables secret? No, and here's how keys leak from Lovable and Bolt apps
Short answer: no. In a Vite app, every environment variable that starts with VITE_ is written into your JavaScript bundle at build time. That bundle is downloaded by every visitor, so anyone can read the value in DevTool
Short answer: no. In a Vite app, every environment variable that starts with VITE_ is written into your JavaScript bundle at build time. That bundle is downloaded by every visitor, so anyone can read the value in DevTools.
The same is true of NEXT_PUBLIC_ in Next.js and REACT_APP_ in Create React App.
Why the prefix exists
Vite hides variables without the VITE_ prefix from your frontend code on purpose. The prefix is how you tell Vite "this value is safe to publish".
Here's how secret keys usually end up public in AI-built apps:
- The AI tool writes
import.meta.env.OPENAI_API_KEY. - It's
undefinedin the browser, so the feature breaks. - The "fix" is to rename it to
VITE_OPENAI_API_KEY. - The feature works, and the key now ships to every visitor.
Lovable, Bolt and most Cursor-built frontends are Vite apps, so this comes up a lot.
Fine in a VITE_ variable
- Your Supabase URL and anon (or
sb_publishable_) key, as long as Row Level Security is on for every table - Stripe publishable keys (
pk_live_,pk_test_) - Firebase config and Google Maps keys restricted to your domain
- Analytics IDs (PostHog
phc_, Sentry DSNs)
Never in a VITE_ variable
- AI keys: OpenAI (
sk-proj-), Anthropic (sk-ant-), Groq, Replicate - Stripe secret keys (
sk_live_) and webhook secrets (whsec_) - The Supabase service_role or
sb_secret_key, which bypasses RLS entirely - Email API keys (Resend
re_, SendGridSG.), AWS keys, database URLs with a password
Not sure which kind a key is? I made a free key checker that tells you what a key is and whether it's allowed in the browser. It runs entirely client-side, so the key never leaves the page.
How to move a key out of the frontend
- Create a server-side function: a Supabase Edge Function, a Netlify or Vercel function, or an API route.
-
Store the key as a server secret with no
VITE_prefix, for examplesupabase secrets set OPENAI_API_KEY=.... - Call your function from the frontend instead of the provider. Check the user is logged in inside the function, so strangers can't use it as a free proxy.
- Rotate the old key. It's in every copy of your old bundle, including CDN and browser caches.
A prompt that works in Lovable, Bolt or Cursor:
Move every call that uses a secret API key out of the frontend into a
Supabase Edge Function that reads the key from Supabase secrets. Remove
the VITE_ variable for it and call the function instead. Only allow
logged-in users to call it. Don't print any keys in your reply.
Check what your live site actually ships
Your .env file isn't what matters. What matters is what ended up in the built JavaScript. Two ways to check:
- Open your live site → DevTools → Sources → search all files (Cmd/Ctrl+Shift+F) for
sk-,sk_live,service_roleandsb_secret_. You should find nothing. - Or paste your URL into LeakPatrol, a free read-only scanner I built. It reads every script your site loads, lists any secret keys (masked), and also checks for public
.envfiles, source maps and missing headers.
The full version of this guide, with a FAQ, is here.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.