VITE_ variables are public: what belongs in them (and what doesn't)
Every variable with a VITE_ prefix is pasted into your JavaScript when you build, so any visitor can read it. This page shows you how to prove that on your own app, which values are fine to ship, and where the secret ones go.
Short answer: Yes, they're exposed. Vite replaces import.meta.env.VITE_* with the literal value at build time, so it ends up in the files your users download. Publishable keys and URLs are fine there. Secret keys belong in a server function's environment, read with no VITE_ prefix.
This is the "why" page for Vite-based apps, including most apps built with Lovable and Bolt. If you already know which key leaked, go to the provider page, such as OpenAI key exposed or Stripe key exposed.
What Vite actually does with VITE_
Vite loads your .env, .env.local, .env.production and similar files when it builds. Variables whose names start with VITE_ become available as import.meta.env.VITE_NAME in client code. During the production build, Vite replaces each reference with the value itself. The prefix exists precisely so you choose what becomes public; Vite's docs say VITE_* variables should not contain sensitive information.
See it on your own build [manual]
Say your .env contains:
VITE_SUPABASE_URL=https://abcd1234.supabase.co
VITE_OPENAI_KEY=sk-proj-EXAMPLE
DATABASE_URL=postgres://...
and a component does fetch(url, { headers: { Authorization: `Bearer ${import.meta.env.VITE_OPENAI_KEY}` } }). Run npm run build, then search the output:
grep -r "sk-proj-" dist/assets/
You'll find sk-proj-EXAMPLE sitting in a minified file such as dist/assets/index-3f9a1c.js. DATABASE_URL won't be there, because it lacks the prefix. That minified file is exactly what your host serves to every visitor.
Safe vs. unsafe in a VITE_ variable
Fine to ship (designed to be public):
- Supabase project URL and anon or
sb_publishable_key, provided Row Level Security is on for every table (see the Supabase RLS checklist). - Stripe
pk_live_publishable key. - Firebase web config, analytics IDs, Sentry DSNs, feature flags.
- Google Maps keys, but only if restricted by HTTP referrer and API in Google Cloud.
Never in a VITE_ variable:
- Supabase
service_roleorsb_secret_keys. - Stripe
sk_live_orrk_live_. - OpenAI, Anthropic, or any other paid AI API key.
- AWS access keys, database URLs, SMTP passwords, webhook signing secrets, admin tokens.
A useful test: if someone with this value could spend your money or read data they shouldn't, it's a secret.
Where secrets go instead
A Vite app is static files with no server of its own, so secret work has to move to a function that runs elsewhere:
- Supabase Edge Functions (common with Lovable and Bolt): set secrets with
supabase secrets set NAME=valueor in the dashboard, and read them withDeno.env.get("NAME"). - Netlify Functions or Vercel Functions: set an environment variable without the
VITE_prefix and readprocess.env.NAMEinside the function. - Cloudflare Workers / Pages Functions:
wrangler secret put NAME, read fromenv.NAME.
List every VITE_ environment variable this app uses and classify each one as public (URLs, publishable or anon keys, analytics IDs) or secret (anything that can spend money, write data or bypass access rules). For each secret one, move the code that uses it into a server-side function that reads it without the VITE_ prefix, make the frontend call that function, and delete the VITE_ variable.
Then rotate every secret that was ever in a VITE_ variable on a deployed build [manual]. Removing it from the code doesn't un-leak the old copy.
Make it stick
- After each build, run
grep -rE "sk_live_|sk-proj-|sk-ant-|service_role|sb_secret_" dist/. Add it to CI so a match fails the build. - Keep
.env*files in.gitignoreand never inpublic/. - Tell your AI builder in its rules: secrets never get a
VITE_prefix.
What the free scan checks here (and what it doesn't)
The scan reads your deployed HTML and up to 25 linked scripts and flags Stripe, OpenAI, Anthropic, AWS, Google and Supabase secret keys by their known formats. It reports a Supabase anon key as info only, since it's public by design, and doesn't test your tables. It also checks whether /.env or /.git/HEAD is publicly served, plus HTTPS and four security headers.
It can't tell that a custom value like VITE_ADMIN_TOKEN is secret. Check those with the classification step above [manual].
FAQ
- Are variables in my .env file secret if they don't start with VITE_?
- From the browser bundle, yes: Vite only exposes VITE_-prefixed variables to client code by default. But the .env file itself must never be deployed to your public web root or committed to a public repo, or the whole file is readable.
- Can I change envPrefix so my secrets stay hidden?
- No. envPrefix only changes which names Vite inlines. Anything your client code reads is in the bundle whatever it's called. Vite's docs warn against an empty prefix for exactly this reason.
- How does the free scan help with VITE_ variables?
- It reads your built page and up to 25 linked scripts, which is where VITE_ values end up, and flags known secret key formats: Stripe, OpenAI, Anthropic, AWS, Google and Supabase secret keys. It can't recognize custom secrets with no known prefix.