Skip to content
All guides

Security3 min read

Keep API keys out of your frontend: a guide for AI-built apps

Which keys are safe in the browser, which never are, how to find leaked ones in your bundle, and what to do if a key has already shipped.

One rule covers most of this guide: anything that reaches the browser is public. It doesn't matter if it's minified, hidden in an environment variable, or buried in a bundle. If the browser can read it, so can anyone.

AI coding tools break this rule often, usually while trying to make an error go away. Here's how to tell which keys are safe, find the ones that aren't, and fix it.

Public keys versus secret keys

Many services give you two kinds of keys, and the names usually tell you which is which.

Safe in the browser (designed to be public, protected by other rules):

  • Supabase anon key, protected by row-level security
  • Stripe publishable key, starting with pk_
  • Firebase web config, protected by Firebase Security Rules
  • Analytics and maps keys that are restricted to your domain

Never in the browser (anyone holding these acts as you):

  • Supabase service_role key
  • Stripe secret key, starting with sk_
  • AI provider keys for OpenAI, Anthropic, and similar services
  • Email, SMS, and database connection strings
  • Any key described as "secret", "private", or "admin"

How keys leak in AI-built apps

Most build tools only send environment variables to the browser if they have a special prefix: VITE_ in Vite, NEXT_PUBLIC_ in Next.js, EXPO_PUBLIC_ in Expo. That prefix means "make this public".

The usual leak looks like this: the AI calls an AI model or Stripe from the frontend, gets an error that the key is undefined, and "fixes" it by renaming OPENAI_API_KEY to VITE_OPENAI_API_KEY. The error disappears, and your key ships to every visitor.

Find leaked keys in five minutes

  1. Search your live site. Open it, open developer tools, go to the Sources (or Debugger) panel, and search across all files for sk_, sk-, service_role, secret, and the names of services you use.
  2. Search your build output. After building, search the output folder, for example grep -rE "sk_live|sk-|service_role" dist/.
  3. Search your environment files for secret keys behind public prefixes.
  4. Search your git history. A key deleted in a later commit is still in the history. Free tools like gitleaks and trufflehog scan every commit.

If a key has already leaked

The order matters:

  1. Rotate the key first. Create a new key and revoke the old one in the provider's dashboard. Removing it from your code doesn't help: copies already exist.
  2. Check the damage. Look at the provider's usage logs and billing for activity you don't recognise.
  3. Move the call to the server (see below) and store the new key only there.
  4. Then clean up your code and environment files.

Rewriting git history to remove a key is optional housekeeping. It doesn't un-leak anything, which is why rotation comes first.

The fix: call secret APIs from the server

Anything that needs a secret key belongs on the server: an API route, a server action, a Supabase Edge Function, or a serverless function. Your frontend calls your server, and your server calls the provider with the secret.

That server route is also where you add the things a browser can't enforce: checking that the user is signed in, validating input, and rate limiting, so one visitor can't spend your monthly AI budget in an afternoon.

Make it hard to do again

  • Keep secrets in your hosting platform's environment settings, never in the repo.
  • Give each key the smallest permissions the provider allows, and set spending limits where you can.
  • Add a secret scanner to your CI so a leaked key fails the build.
  • Tell your AI tool explicitly: "never expose secret keys to the client; call this API from a server route".

Not sure what's in your bundle?

Our free Preflight check looks for exposed keys in your live app's JavaScript. If we find any, we'll tell you which ones and what to rotate first.