ShipReady Guide · Keys and secrets · Next.js · Vite

Which Environment Variables Are Safe in the Browser? (NEXT_PUBLIC_ and VITE_)

A public prefix like NEXT_PUBLIC_ or VITE_ doesn't mean "available to my front end." It means "copied into the JavaScript every visitor downloads." Here's which values can safely live there and which ones are a leak waiting to happen.

6-minute readUpdated Oct 2026All guides

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.

What your source says vs. what visitors download
// 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:

PrefixFramework
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

Fine in the browser

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

Server only

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.

Search the build output
# 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/static

Or 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.
  • .env files 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