08 November 2026

Exposed API Keys in JavaScript: Find, Fix and Rotate Them

By the Ark Crew team · 5 min read

Key next to a browser window with code, illustrating API keys exposed in JavaScript

Here is the rule that prevents most API key leaks: anything your frontend can use, a visitor can read. Minifying the code does not hide it, and neither does putting the key in an environment variable or splitting it into pieces. If the browser needs the key to make a request, the key is in the JavaScript, the Network tab, or both, and anyone can copy it in under a minute. Bots scan public websites and GitHub for keys all day, and a leaked AI or payment key can turn into a large bill or a data breach within hours.

This guide explains how keys end up in frontend code, which keys are safe to ship and which are not, how to check your own site, and what to do if you find one.

How secret keys end up in the browser

Most frameworks copy environment variables with a certain prefix into the JavaScript they send to the browser, at build time:

  • Next.js: variables starting with NEXT_PUBLIC_
  • Vite (and the many apps built on it): VITE_
  • Create React App: REACT_APP_
  • Expo / React Native: EXPO_PUBLIC_

The prefix exists so that you choose, on purpose, what becomes public. The leak happens when a secret gets the prefix anyway, usually because the code calls an API from the browser and the build complained that the variable was undefined. AI coding tools do this a lot: calling OpenAI or Stripe straight from a React component is the shortest way to make a feature work in the preview.

Keys also leak through files that should never be public: a .env file served by the web server, source maps that include server code, or a public Git repository with a committed .env in its history.

Which keys are fine in the browser

Some keys are designed to be public. They identify your project, and the real protection lives somewhere else:

  • Supabase anon or sb_publishable_ keys, as long as Row Level Security protects every table (see our Supabase RLS guide).
  • Firebase web config, including its apiKey, as long as your Security Rules are strict (see our Firebase rules guide). Restrict the key to your domains and consider App Check.
  • Stripe publishable keys starting with pk_live_ or pk_test_.
  • Google Maps browser keys, restricted by HTTP referrer to your own domains.
  • Analytics IDs and site keys for CAPTCHAs such as reCAPTCHA or Turnstile.

Which keys must never be in the browser

  • Stripe secret keys (sk_live_) and restricted keys (rk_live_)
  • OpenAI keys (sk- and sk-proj-) and keys of other AI providers
  • Supabase service_role or sb_secret_ keys, which bypass all your database rules
  • AWS access keys (AKIA followed by 16 characters) and their secret keys
  • SendGrid (SG.), Mailgun, Twilio and other email or SMS keys
  • GitHub tokens (ghp_, github_pat_) and other deployment tokens
  • Database connection strings with passwords, JWT signing secrets and webhook signing secrets (whsec_)

How to check your own site

In the browser. Open your live site, press F12, go to Sources and press Ctrl+Shift+F (Cmd+Option+F on a Mac) to search every loaded file. Search for sk_live, sk-proj, sk-, service_role, AKIA, secret and token. Then use the app normally with the Network tab open: a request from the browser directly to api.openai.com, api.stripe.com or similar means your secret key is on the page.

In your build output. After building, search the files that get deployed:

grep -rEo "sk_live_[0-9A-Za-z]{10,}|sk-proj-[0-9A-Za-z_-]{20,}|AKIA[0-9A-Z]{16}|sb_secret_[0-9A-Za-z_-]{10,}" \
  .next/static dist build 2>/dev/null

In your Git history. A key you deleted from the code is still in every earlier commit. Search for each prefix you use:

git log --all -p -S "sk_live_" -- .

Tools such as gitleaks and trufflehog scan a whole repository history for hundreds of key formats, and GitHub's secret scanning can block pushes that contain keys.

From the outside. Our free Website Health Check reads the JavaScript your homepage loads, the way a browser does, and looks for common secret-key formats, along with security headers and outdated libraries.

Found one? Fix it in this order

1. Rotate the key first. Revoke the leaked key in the provider's dashboard and create a new one. Assume the old key has already been copied: removing it from your code does nothing for the copies. Check the provider's usage and billing pages for activity you do not recognise.

2. Move the call to the server. The browser calls your own endpoint, and your endpoint calls the provider with the key from a server-only environment variable (no public prefix). In Next.js, for example:

// pages/api/summarize.js: runs on the server, so the key never reaches the browser
export default async function handler(req, res) {
  if (req.method !== "POST") return res.status(405).end();

  const user = await getSignedInUser(req); // your own session check
  if (!user) return res.status(401).json({ error: "Please sign in" });

  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    },
    body: JSON.stringify({
      model: process.env.AI_MODEL,
      messages: [{ role: "user", content: String(req.body.text || "").slice(0, 4000) }],
    }),
  });
  res.status(response.status).json(await response.json());
}

The same pattern works with a Supabase Edge Function, a Firebase Cloud Function or any small serverless function.

3. Protect the new endpoint. A server route that calls an AI provider for anyone who asks is the same leak with extra steps. Require a signed-in user, limit how often each user and IP address can call it, cap the input size, and set a monthly spending limit with the provider.

4. Clean up. Remove the old variable everywhere, rebuild and redeploy, and run the checks above again. If the key was in a public repository, rewriting the history is optional once the key is rotated, but do it if the repository will stay public.

Prevent the next leak

  • Never give a secret a public prefix. If the build says it is undefined in the browser, that is the framework protecting you.
  • Keep .env files in .gitignore, and keep secrets in your host's environment settings.
  • Use separate keys for development and production, with the least access each one needs.
  • Turn on secret scanning in your repository and spending alerts with every paid API.

Need help?

Exposed keys are one of the first things we check in our AI app security audit, along with database rules, access between users, admin routes and payments. You get a plain-English report with proof and fixes, and we can make the fixes for you by pull request. If your app was built with an AI coding tool, our pre-launch security checklist covers the rest of the common problems.

Frequently asked questions

Are environment variables private in React and Next.js apps?

Only the ones used on the server. Variables with a public prefix, such as NEXT_PUBLIC_, VITE_, REACT_APP_ or EXPO_PUBLIC_, are copied into the JavaScript sent to the browser, so anyone can read them.

What should I do if my API key was exposed?

Revoke and rotate the key in the provider's dashboard first and check its usage and billing for activity you do not recognise. Then move the call to a server route, and protect that route with a sign-in check and rate limits.

Which API keys are safe to use in the browser?

Keys designed to be public: Stripe publishable keys, the Supabase anon or publishable key with Row Level Security, the Firebase web config with strict Security Rules, and Google Maps keys restricted to your own domains.

Need help with AI App Security Audit?

Built your app with Lovable, Bolt, Cursor, v0 or Replit? We check it the way an attacker would, with your written OK, and show you what to fix. Tell us what you want to build: we reply within 24 hours with honest advice and a clear quote.

Start a ProjectSee our AI App Security Audit services →

More from our blog