Skip to content
Shawon
All posts

4 min read

You cannot hide an API key in frontend code

Not in a constant, not in an env file, not behind a build step. If the browser can use it, the browser can show it — and so can anyone who opens DevTools.

Every frontend developer hits this moment. The API needs a key, the key is sitting in your code, and something about that feels wrong. So you move it into .env, the warning feeling goes away, and you move on.

It should not have gone away. Moving a key into .env changes where you type it. It does not change who can read it.

What a build step actually does

Frontend bundlers do not fetch environment variables at runtime. There is no runtime on a server to fetch them from — the code is running on someone else's phone. So at build time the bundler finds every reference to an exposed variable and pastes the value in.

You write:

const API_KEY = process.env.NEXT_PUBLIC_API_KEY;

The browser receives:

const API_KEY = "abc123def456...";

The same happens with VITE_ in Vite and REACT_APP_ in Create React App. The prefix is not protection. It is the opposite — it is you telling the bundler this one is safe to publish, and the bundler doing as it is told.

Open DevTools, go to Sources, search the bundle. The key is there in full, on every device that ever loaded your site.

The tricks that do not work

Base64 encoding it. Base64 is not encryption. It is an alphabet. One function call turns it back.

Splitting it across files and joining at runtime. Both halves ship. The join happens in code anyone can read.

Obfuscating the bundle. The key still has to be a real string by the time it reaches the HTTP request, and the Network tab shows every request with its headers.

Putting it in a config file instead of code. Same bundle, same browser, same DevTools.

They all share one flaw: for the browser to send the key, the browser must hold the key. Anything the browser holds, its owner can see. This is not a weakness in a particular tool — it is what a browser is.

How to tell if you have this problem

Three checks, two minutes:

  1. Grep your frontend for likely names — apiKey, API_KEY, secret, token, Authorization. Anything with a long literal string next to it.
  2. Open the deployed site, DevTools, Sources, search for part of the key. If it appears, it is public.
  3. Check the Network tab. Expand any request to your API and read the headers. Whatever is in there, visitors can read too.

The second check is the one that settles arguments. Nothing is theoretical once you are looking at your own key in your own browser.

What to do instead

Put the credential on a server you control. The browser calls your backend, your backend calls the third-party API with the key, and the key never leaves your server. This is what an API route, a server action, or a small proxy endpoint is for.

Give the browser a session token, not an API key. When someone signs in, your server issues a token tied to that one user, with a short life. If it leaks, you revoke one session instead of rotating a key every client is using.

Check authorisation on every request. A session token says who is asking. It does not say they are allowed. Your server still has to decide whether this particular user may read this particular record, and that check belongs on the server — never in the interface that hides a button.

Rate limit the proxy. The moment you put an endpoint in front of a paid API, it is a way to spend your money. If it needs no authentication, assume a script will find it.

If you discover one in production

Two things, in this order.

Rotate the key. A key that has been in a frontend bundle must be treated as known, whether or not you have evidence it was used. There is no "probably fine" here — the bundle was public.

Tell whoever owns the system. If it is a client project or a team repository, this is not a quiet fix. Someone needs to decide whether the exposure has to be disclosed, and that is not a decision for whoever happens to notice it.

Then do the actual repair: move the call to a server, issue tokens, and remove the key from the codebase. If the key is scattered across many files, a single search-and-replace is safer than fixing them one at a time — the one you miss is the one that leaks the new key.

And remember that git keeps history. Removing a key from the current code does not remove it from the commit it was added in. The rotation is the part that matters.

A simpler test

Before you put a credential in frontend code, ask one question:

Am I comfortable printing this on the homepage?

If the answer is no, it does not belong in the browser. There is no middle setting.

security · frontend · api