The short version
- Public prefix = public value. The build replaces the variable with its literal value in your bundle.
- Safe: keys designed to identify your project, like Stripe
pk_, Supabase anon or publishable keys (with RLS on), Firebase web config, and analytics IDs. - Never safe: anything that spends money, sends messages or bypasses access rules: AI keys, Stripe
sk_, Supabase service role, database URLs. - The fix is a server route that holds the secret, not a cleverer variable name.
1. How public env variables actually work
When you build your app, the bundler finds every reference to a public variable and swaps in the actual value as plain text. There's no runtime lookup and no hiding. If you open the built JavaScript, the value is sitting there.
// Your source code
const key = import.meta.env.VITE_OPENAI_KEY;
// The built file every visitor downloads
const key = "sk-proj-AbC123...";Each framework has its own prefix, but they all behave this way:
| Prefix | Framework |
|---|---|
| NEXT_PUBLIC_ | Next.js |
| VITE_ | Vite, which most Lovable, Bolt and React apps use |
| REACT_APP_ | Create React App |
| EXPO_PUBLIC_ | Expo / React Native |
| PUBLIC_ | Astro and SvelteKit |
In Vite, don't widen envPrefix in your config to expose more variables. That only makes it easier to publish a secret by accident.
2. Safe vs. never-safe keys
Stripe publishable key (pk_)
Supabase anon or sb_publishable_ key, with RLS on
Firebase web config, with security rules set
Google Maps key, restricted to your domain
Analytics IDs (Google Analytics, PostHog, Plausible)
Sentry DSN
OpenAI, Anthropic and other AI keys
Stripe sk_, rk_, whsec_
Supabase service role or sb_secret_DATABASE_URL and database passwords
Email and SMS keys (Resend, SendGrid, Twilio)
JWT secrets, AWS keys, GitHub tokens
The test: if a stranger had this value, could they spend your money, send messages as you, or read data your access rules would block? If yes, it's server only. Public keys are safe because the real protection sits elsewhere: Supabase relies on RLS, Firebase on security rules, Google Maps on domain restrictions.
3. Check your build in one command
Search what you actually ship, not your source. Source can look clean while the build inlines a secret from your env file.
# Vite: build, then search dist/
npm run build
grep -rnoE "sk-[A-Za-z0-9_-]{20,}|sk_(live|test)_|service_role|sb_secret_|postgres(ql)?://" dist
# Next.js: search the client bundles
npm run build
grep -rnoE "sk-[A-Za-z0-9_-]{20,}|sk_(live|test)_|service_role|sb_secret_|postgres(ql)?://" .next/staticOr scan your live site with ShipReady, which checks the JavaScript your visitors actually download.
4. Moving a secret off the browser
Renaming VITE_OPENAI_KEY to OPENAI_KEY stops it being bundled, but then your front end can't make the call at all. The real fix is to move the call:
- Next.js: put the call in a Route Handler or Server Action. Server code can read variables without the public prefix.
- Vite apps with Supabase (Lovable, Bolt): use a Supabase Edge Function and store the key with Supabase secrets. The browser calls the function; the function calls the API.
- Anything else: a small serverless function (Vercel, Netlify or Cloudflare) does the same job.
If the secret was ever deployed with a public prefix, rotate it as well. Moving it doesn't un-leak the old value.
Before you ship
- Every public-prefixed variable is on the safe list.
- A search of your build output finds no secret key patterns.
- Calls that need a secret go through a server route or edge function.
- Any secret that was ever bundled has been rotated.
.envfiles are in.gitignore.
Check your live site in 60 seconds
ShipReady scans your deployed URL and the JavaScript visitors actually download: leaked keys, Supabase exposure, unprotected API routes, titles and share images. Secret findings unlock only after you verify you own the site.
Free scan, no signup, no code upload.FAQ
Is my Supabase anon key really safe to expose?
Yes, that's its job, but only if row-level security is on with sensible policies. Without RLS the anon key can read and write your tables. See the RLS guide.
Can I obfuscate or encrypt the key in the front end?
No. The browser has to decrypt it to use it, so anyone can do the same. Encoding or splitting a key only slows down a person for a minute.
My AI builder put the key in a VITE_ variable. What do I tell it?
Ask it to "move the API call into a Supabase Edge Function (or server route) that reads the key from a server-side secret, and have the front end call that function instead." Then rotate the old key.
Keep reading
Sources: Next.js: Environment variables · Vite: Env variables and modes