KeyDrift
Free scan
All posts

Your .env file is not the problem

Git-ignoring your environment file protects the repository, not the bundle. Here is what actually ends up in the JavaScript you serve.

KeyDrift4 min read
secretsbundles

There is a reassuring ritual in every new project. You put your keys in .env, you check that .env is in .gitignore, and you move on. The repository stays clean. Every secret scanner in CI passes.

And then the key ships to every visitor who loads your site.

What the prefix actually does

Frontend build tools do not have a runtime environment in the browser. There is no process.env on a user's machine. So when you write this:

const client = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_KEY,
);

the bundler does not pass the variable through. It performs a textual substitution at build time. The literal string value is written into the output JavaScript, and that file is served from your CDN to anyone who asks for it.

This is documented, intended behaviour. NEXT_PUBLIC_ in Next.js and VITE_ in Vite both mean "inline this value into the client bundle". The prefix is the opt-in.

The problem is not the mechanism. The problem is which variable ends up behind the prefix.

How the wrong key gets there

Consider a common sequence. You are building a feature that needs to read a table. The anon key returns nothing, because row-level security is doing its job. The feature does not work.

The fastest way to make it work is to use the service-role key instead. It bypasses RLS entirely, and the feature immediately works. To use it in client code, it needs a NEXT_PUBLIC_ prefix — otherwise it is undefined in the browser.

So NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY gets created, the feature ships, and the tests pass.

AI coding assistants make this sequence faster, not different. Asked to make a failing query work, the shortest correct-looking path is the elevated credential. The generated diff contains a variable name, not a secret. Nothing in review looks wrong, because in the source, nothing is wrong.

Why your scanners miss it

Almost every secret-scanning tool works on one of two things: your git history, or your working tree. Both are the wrong place to look.

  • The repository is clean. The .env was never committed. The scanner is correct when it reports nothing.
  • The source is clean. The code contains process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY — an identifier, not a credential. There is no high-entropy string to match on.
  • The build output is not scanned. It is generated on a CI runner, uploaded to a CDN, and never inspected by anything.

The secret only exists in one place: the artefact. And nothing is looking at the artefact.

What to check instead

You can do this yourself, right now, without any tooling. Open your deployed site, view source, and open one of the JavaScript chunks. Search it for eyJ — the prefix of every JWT, which is what a Supabase key is. Search for sk_live, sk-, AKIA.

If you find something, note that the fix is not to remove the string from the bundle. The key is already compromised. Anyone who loaded that page has it, and CDN caches and archives may still serve it after you deploy again. The fix is:

  1. Rotate the key at the provider, immediately.
  2. Move the call that needed it to a server route, an edge function or an RPC.
  3. Give the client the publishable key and let RLS do the work it was there to do.
  4. Redeploy, then check the new bundle to confirm the value is gone.

Not every key in a bundle is a leak

Worth saying clearly, because the opposite mistake is also expensive: some credentials belong in client-side JavaScript.

A Supabase anon key, a Stripe publishable key, a Mapbox public token, a Sentry DSN — these are designed to be public. They have recognisable formats. Reporting them as leaks trains people to ignore the tool that reported them, which is worse than not scanning at all.

The useful question is never "is there a key in the bundle". It is "is there a key in the bundle that grants more than a stranger should have".

The short version

Git-ignoring .env protects your repository. It does nothing for your bundle, because the bundle is built from the values, not from the file. If you have never looked at what your deployed JavaScript actually contains, that is the gap — and it takes about two minutes to close.

KeyDrift reads the JavaScript your app actually serves and finds the Supabase, Stripe, OpenAI and AWS keys that should never have left your server. Run a free scan.

KeyDrift is a Veristria product. More about KeyDrift.