An API key leaked into your app. What to do in the next ten minutes
The instinct is to delete the line and redeploy. That fixes the file and does nothing about the key, which is the part that’s actually exposed. Order matters more than speed here.
Why deleting it isn’t enough
A key in your frontend code has been downloaded by every visitor, every bot and every crawler that has loaded your site. It may sit in someone’s browser cache, in a CDN edge node, in the Internet Archive, and — if your repo is public — in git history, where deleting the current version changes nothing.
The only action that closes it is revoking the key at the provider. Everything else is housekeeping.
The order
- 01
Rotate the key
In the provider's dashboard — Stripe, OpenAI, Supabase, wherever it came from — issue a new key and revoke the old one. Not later, not after you've tidied the code. The old key works until you revoke it, and everything else you do is theatre until this is done.
- 02
Put the new one somewhere the browser can't reach
An environment variable read by a server route or an edge function. If your builder offers 'secrets' or 'environment variables', that's the place. If the value ends up anywhere in your frontend code, you've just repeated the mistake with a fresher key.
- 03
Move the call, not just the key
The reason the key was in the browser is that the browser was making the call. Moving the key without moving the call breaks your app, so you'll be tempted to move it back. The request has to happen on a server that holds the key and returns only the result.
- 04
Check what it was used for
Provider dashboards keep request logs. Look at the window between when the key shipped and when you rotated it, for usage you can't account for — unfamiliar volumes, unfamiliar times, requests you didn't write.
- 05
Re-check the live app
Not the code — the deployed site. Old builds, cached bundles and preview deployments can outlive the fix. The only thing that proves it's gone is fetching the site the way a stranger does and finding nothing.
Which keys actually matter
Not every key in your bundle is a problem, and panicking about the harmless ones makes it harder to see the real one.
- Meant to be public — Stripe publishable keys (pk_live_…), Supabase anon keys, Firebase web config, Google Maps keys restricted to your domain. These are designed to ship. Check the restrictions, don't panic.
- Never public — Anything labelled secret, service_role, private or admin — sk_live_…, sb_secret_…, service_role JWTs, database connection strings with a password in them. One of these in a bundle is an emergency.
A key that’s meant to be public can still be misconfigured. A Google API key with no domain restriction is someone else’s free quota; a Firebase config with open database rules is the same problem as an open Supabase table.
How it got there
Almost always the same way: you asked for a feature that needed a third-party service, and the fastest thing that worked was calling that service straight from the page. It ran, the preview looked right, and nothing anywhere said “this value is now public.”
Assay checks for this automatically, from outside your app, with no login and no access to your code — and tells you in plain English what it found, with the fix attached. What it doesn’t check is written down too.
Read next
- Why Lovable apps ship with the database openRow Level Security is off while you build because it has to be, and nothing ever turns it back on. The mechanism, a 30-second self-check, and the exact fix.
- Which Supabase keys are safe in the browserThe anon key is meant to be public. The service_role key ends your security model. How to tell them apart in ten seconds, and what to do if the wrong one shipped.
- Is my AI-built app safe to publish? A checklist you can run yourselfSeven things to check before you put a live URL in front of real users, in the order that matters. No tools required, though we'll check them for you if you'd rather.