Skip to main content
Security7 min readPublished September 19, 2026Updated September 22, 2026

How to keep API keys out of your frontend

Any value used by code that runs in the browser is public, no matter how it is stored. The fix is not obfuscation — it is moving the call that uses the secret to a server function, and having the browser call your own endpoint instead of the third-party service.

Why can't a key be hidden in the browser?

Everything the browser runs, the visitor can read: the bundled JavaScript, the network requests, the memory in the developer tools. Minification renames variables; it does not hide strings. Encoding a key only means an attacker copies your decoding function too.

What does the fix look like?

Before — the key ships to every visitor
// component.tsx (runs in the browser)
const res = await fetch("https://api.provider.com/v1/send", {
  headers: { Authorization: `Bearer ${import.meta.env.VITE_PROVIDER_KEY}` },
});
After — the browser calls your server, the server holds the key
// send.functions.ts (runs on the server)
export const send = createServerFn({ method: "POST" })
  .inputValidator((data) => schema.parse(data))
  .handler(async ({ data }) => {
    const key = process.env.PROVIDER_KEY;
    if (!key) throw new Error("PROVIDER_KEY is not set");
    const res = await fetch("https://api.provider.com/v1/send", {
      method: "POST",
      headers: { Authorization: `Bearer ${key}` },
      body: JSON.stringify(data),
    });
    if (!res.ok) throw new Error(`Provider failed [${res.status}]: ${await res.text()}`);
    return res.json();
  });

The endpoint you just created is now the thing that needs protecting: validate its input, and check who is calling it if the action costs money or touches private data.

How do I check what is actually exposed?

  1. 1Open your live site, then view the page source and the bundled JavaScript files.
  2. 2Search those files for your key's first several characters, and for prefixes like sk_ and service_role.
  3. 3Open the Network tab and inspect the request headers your browser sends to third parties.
  4. 4Search your repository — including old commits — for the same strings.
  5. 5Check your provider's usage dashboard for traffic you cannot explain.

A key found in any of those places is compromised. Rotate it first, then fix the code — in that order.

Which keys are meant to be public?

Publishable and anonymous keys are designed to be seen, and the protection lives in server-side rules such as database access policies. Seeing one in your bundle is expected and fine. The danger is assuming the same about a secret key because both arrived in the same email.

How do you keep it from happening again?

  • Name secrets without a public prefix so a build tool refuses to expose them.
  • Read secrets only inside server functions, never in shared modules.
  • Fail loudly when a secret is missing, so misconfiguration is obvious.
  • Enable your provider's secret scanning on the repository.
  • Review any AI-generated code that adds a new integration — that is where it slips in.

Frequently asked questions

Is it enough to restrict the key by domain?
It reduces the damage and is worth doing, but domain checks are not a substitute for keeping secrets off the client.
What if the provider has no server-side option?
Proxy it. Your server calls them; your browser calls your server. That is always available.
Do I need to rotate if the repository is private?
If the key only ever lived in a private repository and never reached the browser, rotation is a judgement call. If it was ever in a public bundle, rotate.

Practice this in MessyDev

Reading it once helps. Doing it once sticks. These are the hands-on parts of MessyDev that cover the same ground.

Keep going