NEXT_PUBLIC_ and VITE_ variables are shipped to every visitor
By Pritesh Ghodge, security engineer · Last updated 24 October 2026
Want to know if your live app is leaking a key right now?
Abbi Labs checks your deployed JavaScript for secret keys, plus open Supabase tables and exposed .env files. The quick check is free and needs no sign-up.
Weekly AI-app security checklist
One short email a week: a practical check for apps built with Supabase, Next.js and AI tools (RLS, leaked keys, headers and more). Unsubscribe anytime.
If a variable starts with NEXT_PUBLIC_ (Next.js) or VITE_ (Vite, which is what Lovable, Bolt and many other AI builders use), its value is written into the JavaScript your site sends to every visitor. That's the whole point of the prefix. It's fine for public keys and a leak for secret ones. Here's how it works, how to check your own site, and how to fix it.
How build-time inlining works
When your app is built, the bundler finds every reference to a public variable and swaps in the literal value. Your code says this:
const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_KEY);
// or, in a Vite app:
const openai = new OpenAI({ apiKey: import.meta.env.VITE_OPENAI_KEY });and the file that ships to the browser says something like this:
const stripe = new Stripe("sk_live_51H…");
const openai = new OpenAI({ apiKey: "sk-proj-…" });Minifying doesn't hide it. The string is still there for anyone who opens DevTools or downloads the file.
The common way a secret ends up there: an AI agent writes a call to Stripe or OpenAI from a browser component, it fails because the variable is undefined in the browser, and the "fix" is to add the public prefix. The error goes away and the key ships with the next deploy.
Next.js has one more route in: values listed under env in next.config.js are inlined into the bundle too, with or without the prefix.
Which keys are fine to expose and which aren't
| Usually fine in the browser | Never in the browser |
|---|---|
Supabase URL and anon / sb_publishable_ key (RLS protects your data) | Supabase service_role key or sb_secret_ keys |
Stripe publishable key (pk_live_, pk_test_) | Stripe secret or restricted keys (sk_live_, rk_live_) |
| Firebase web config and Google Maps keys, restricted by referrer and API | OpenAI (sk-), Anthropic (sk-ant-) and other AI provider keys |
| Analytics and error-tracking keys designed for the browser | AWS access keys, GitHub tokens, Slack tokens, email API keys (e.g. Resend re_), private keys, database connection strings |
A simple rule: if the key can spend money, send email, or read or change data without a user's own login, it belongs on a server.
"Fine to expose" still has conditions. A public Supabase key is only safe if Row Level Security is set up properly (see how to check your Supabase tables), and Google keys should be restricted in Google Cloud Console.
How to find leaks in your deployed site
Check the live site, not your repo. The bundle is what attackers see, and it reflects whatever was built, including variables set in your host's dashboard.
- Open your production site in Chrome, then DevTools → Sources.
- Search across all files with Ctrl+Shift+F (Cmd+Option+F on a Mac).
- Search for
sk_live,rk_live,sk-,sk-ant-,service_role,sb_secret_,AKIA,ghp_,xoxandBEGIN PRIVATE KEY. - Search for
eyJtoo. Long strings starting with it are JWTs. Decode the middle part and read therole:anonis fine,service_roleis a leak. - Click around the app first, especially logged-in pages. Code for other pages often loads later in separate chunks, and DevTools only searches files that have loaded.
From a terminal you can do a rough version for a Next.js site:
curl -s https://yourapp.com/ | grep -oE '/_next/static/[^"]+\.js' | sort -u \
| while read f; do curl -s "https://yourapp.com$f" \
| grep -oE '\b(sk_live_[A-Za-z0-9]{8}|sk-(proj-)?[A-Za-z0-9_-]{8}|sb_secret_[A-Za-z0-9_-]{6})' \
&& echo " ^ in $f"; doneIt prints only the first few characters of each match, which is enough to know you have a problem. For a Vite app, look for files under /assets/ instead.
Also check whether your site serves source maps (.js.map files). They can contain your original source, comments and all, so a key that was removed from the minified file can still be sitting in the map.
How to fix a leaked key
- Revoke or rotate the key first with the provider (Stripe, OpenAI, Supabase and so on). Removing it from the code doesn't make the old one safe. For Supabase keys, follow the service_role key guide so you don't break your app.
- Rename the variable without the prefix, for example
STRIPE_SECRET_KEYinstead ofNEXT_PUBLIC_STRIPE_KEY, and set the new value in your host's environment settings. - Move the call to the server. In Next.js, use a Route Handler or Server Action. Add
import "server-only"to any module that reads secrets, so the build fails if a client component imports it. - In Lovable, Bolt and other Vite apps, there's no server in the bundle, so move the call into a Supabase Edge Function (or another backend) and store the key as a function secret.
- Add the checks the key used to skip. A server route that calls OpenAI for anyone who asks is still an open wallet. Check the user is signed in and add a rate limit or spending cap.
- Redeploy and search the live bundle again to confirm the key is gone.
A prompt you can paste into your AI builder:
Security fix: secret keys must never be in client code.
1. List every env variable with a NEXT_PUBLIC_ or VITE_ prefix and say
whether each one is public by design or a secret.
2. For each secret, remove the prefix and move every call that uses it
into a server route / edge function that checks the user's session.
3. Don't add the prefix back to fix an "undefined" error.
Show me the list and the diff before applying.Why repo scanners miss this
Secret scanners for git look for keys committed to your code. With env variables done "properly", the key is never in your repo. It lives in your host's settings and gets pulled into the bundle at build time. The repo looks clean while the live site serves the key. That's why the check has to run against the deployed app, and run again after each deploy, because every deploy is a fresh build.
Let Abbi Labs check it for you
Abbi Labs scans your live app from the outside. On domains you've verified you own, it reads your home page, the JavaScript files it links to and any public source maps, and flags secret-key patterns. It also checks Supabase tables and storage buckets against your public key, exposed .env and .git files, and security headers.
It only sees what the home page loads, so keys in code that only loads on other pages need the manual search above. The first scan is free, and Monitor re-scans weekly and after every deploy.
The quick check on our home page needs no sign-up and covers your home page JavaScript, Supabase tables, /.env, /.git and headers. Source maps and storage buckets are in the full scan after you verify your domain.
Check my site free, no sign-up →
FAQ
Are NEXT_PUBLIC_ variables visible to users?
Yes. Next.js replaces every process.env.NEXT_PUBLIC_ reference with its literal value when it builds your app, so the value is written into the JavaScript files every visitor downloads. Anyone can read it in DevTools.
Are VITE_ variables secret?
No. Vite exposes variables that start with VITE_ (or whatever envPrefix is set to) on import.meta.env and replaces them with their values at build time. They end up in the client bundle. Lovable, Bolt and other Vite-based apps work the same way.
Is my .env file safe if it's in .gitignore?
Keeping .env out of git stops it leaking through your repo. It does nothing for prefixed variables: the build copies NEXT_PUBLIC_ and VITE_ values into your JavaScript regardless of where they were stored.
I removed the variable and redeployed. Is the key safe now?
No. Anyone who loaded the old version may have copied it, and old builds can stay cached or reachable. Revoke or rotate the key with the provider, then deploy the fix.
Does Abbi Labs check for this?
Yes. On domains you've verified you own, it reads your home page, the same-origin JavaScript files it links to and any public source maps, and looks for secret-key patterns such as Stripe secret keys, OpenAI and Anthropic keys, Supabase service_role and sb_secret_ keys, AWS access key IDs, GitHub and Slack tokens, Resend keys and private key blocks. Reports show only a masked version of any key.
Related guides
- OpenAI, Claude or Gemini API key exposed in your frontend? What to do now
- Lovable app security checklist (from a security engineer)
- Supabase service_role key exposed? Do this in the next 15 minutes
- How to check if your Supabase tables are readable by anyone
- 3 files your production site shouldn't be serving: .env, .git and source maps
- Security headers for Lovable, Bolt, v0 and Cursor apps: CSP, HSTS and clickjacking
- Supabase "42501 permission denied for table": fix it with a GRANT, not by turning off RLS