When an AI builder needs to call OpenAI, Stripe or a database, the shortest path is to put the key right next to the code that uses it. If that code runs in the browser, the key ships to every visitor. It works perfectly in the preview, so nobody notices.
Anyone who opens your site can read everything the browser downloads. That includes your JavaScript bundles and every value baked into them. This guide shows how to check your own app in about fifteen minutes, without reading the whole codebase.
Which keys are safe to publish and which are not
Not every key in the frontend is a problem. Some are designed to be public, and the protection lives somewhere else. The trouble starts when a secret key ends up where a public one belongs.
- Usually fine in the browser: a Stripe publishable key (pk_live_ or pk_test_), a Supabase anon or publishable key, a Firebase web config, a Google Maps key restricted to your domain.
- Never fine in the browser: a Stripe secret key (sk_live_ or sk_test_), a Supabase service_role or secret key, OpenAI or Anthropic API keys, database connection strings, email provider keys, webhook signing secrets.
- Depends on setup: third-party keys that claim to be public but have no domain or usage restrictions. Treat them as secret until you have configured limits.
A simple rule: if the key lets someone spend your money, read private data or act as an admin, it must only exist on a server.
Step 1: search what your live site actually ships
Start with the deployed app, not the source code. The bundle is what attackers see, and it can contain values you forgot about.
- Open your live site in Chrome or Firefox and open developer tools.
- Go to the Sources tab (Debugger in Firefox) and look through the JavaScript files your site loaded.
- Use the search across all files (Ctrl+Shift+F, or Cmd+Option+F on a Mac) and search for the patterns below one at a time.
- Also check the Network tab while you use the app. Look at request headers for Authorization values that contain long-lived secret keys.
sk_live_
sk_test_
sk-
sb_secret_
service_role
secret
api_key
apiKey
Bearer
postgres://
mongodb+srv://The pattern sk- catches the common OpenAI key format. The patterns sb_secret_ and service_role catch Supabase's newer secret keys and variable names like SUPABASE_SERVICE_ROLE_KEY. Older Supabase keys are long tokens starting with eyJ, and the role inside is only visible once decoded, so decode any you find locally and check whether it says anon or service_role. A hit is not always a leak, so read the surrounding code before panicking.
Step 2: check your environment variables
Most frameworks expose variables to the browser based on a prefix. Anything with these prefixes is bundled into client code and is effectively public:
- NEXT_PUBLIC_ in Next.js
- VITE_ in Vite projects, which includes many Lovable and Bolt apps
- EXPO_PUBLIC_ in Expo and React Native
- REACT_APP_ in older Create React App projects
Open your .env files and your hosting dashboard. If you see something like VITE_OPENAI_API_KEY or NEXT_PUBLIC_STRIPE_SECRET_KEY, that secret is in your bundle right now. The prefix was added to make an error go away, and it made the key public instead.
Step 3: check your Git history
Deleting a key from your code does not remove it from history. If the repository was ever public, or might become public, anything committed is exposed. Run this from your project folder to search every commit:
git log -p --all | grep -nE "sk_live_|sk-[A-Za-z0-9]|sb_secret_|service_role|BEGIN PRIVATE KEY"For a more thorough scan, open-source tools like gitleaks and trufflehog search history for hundreds of key formats. GitHub also runs secret scanning on public repositories and can alert you when a known key format is pushed.
Step 4: if you found a leak, rotate first
The order matters. Fixing the code without replacing the key leaves the old one working for whoever already copied it.
- Generate a new key in the provider's dashboard.
- Put the new key in a server-only environment variable, with no public prefix.
- Deploy the change that moves the call to the server.
- Revoke the old key so it stops working.
- Check the provider's usage and billing pages for activity you don't recognize.
Step 5: move the call to the server
The lasting fix is architectural. The browser calls your own backend, and your backend calls the third-party API with the secret key. Depending on your stack, that backend can be a Next.js route handler or server action, a Supabase Edge Function, a Firebase Cloud Function, or a small serverless function on your host.
While you're there, add the checks the browser could never enforce: confirm the user is signed in, limit how often they can call the endpoint, and cap request sizes. An OpenAI proxy with no auth or rate limit is just a public key with extra steps.
Keep it from happening again
- Add a secret scanner to your repository so commits containing keys are flagged before they land.
- Set spending limits and usage alerts with every paid API provider.
- Restrict public keys by domain or app where the provider supports it.
- When prompting an AI builder, say explicitly that secret keys must stay on the server, and review any change that touches environment variables.
Finding and moving exposed keys is one of the first things a Deeraf Tech Check covers, alongside the database and auth checks that usually come with it. If something leaked, we can rotate and fix it in a short Build Sprint.