Guides · Leaked secret keys
Is your app leaking secret keys?
An API key in your app’s pages is readable by every visitor, and by every bot that scans the web for them. Some keys are designed for that. Others let a stranger charge your card, read your whole database or run up your AI bill. Here is how to tell them apart, check your app, and fix a leak.
Safe in the browser, and never
| Key | In the browser? | Why |
|---|---|---|
| Supabase anon or publishable key | Fine | It identifies your project. What it can do is limited by row-level security, which is why those rules matter. |
Firebase web config (apiKey) | Fine | Same idea: your security rules decide what it can reach. |
Stripe publishable key (pk_live_…) | Fine | It can start a payment, but can’t charge, refund or read your customers. |
Stripe secret or restricted key (sk_live_…, rk_live_…) | Never | Refunds, payouts, customer data: whatever the key allows. |
Supabase service-role or secret key (sb_secret_…) | Never | It ignores every row-level security rule. Your whole database, read and write. |
| OpenAI, Anthropic and other AI keys | Never | Anyone can spend your credit, and bots look for exactly these. |
| Email, GitHub and other service tokens | Never | Send mail as you, read or change your code. |
| Database connection strings with a password | Never | Direct access to the database, around every rule. |
How keys end up in apps built with AI
- The prefix. In Vite apps (what Lovable and Bolt build), any variable starting with
VITE_is copied into the code every browser downloads; in Next.js,NEXT_PUBLIC_does the same. AVITE_STRIPE_SECRET_KEYis a public key with a misleading name. - The shortcut. Asked to “add an AI chat”, an AI builder may call OpenAI straight from the page, with the key next to it. It works in the preview, so nobody notices.
- The paste. You pasted a key into the chat to get something working, and it went into a file as a plain string.
- The repository. Lovable and Bolt can sync your project to GitHub. If that repository is public, every key ever committed to it is public, including ones you later deleted.
Check your app
- The quick way: run the free launch check on your live address. It reads the page and up to ten of its scripts, and looks for Stripe secret and restricted keys, OpenAI and Anthropic keys, Supabase service-role and secret keys, GitHub tokens, SendGrid keys and private keys. A key it finds is shown masked.
- By hand: open your live app in Chrome, open the developer tools (right-click, Inspect), and press Ctrl+Shift+F (Cmd+Option+F on a Mac) to search every loaded file. Search for
sk_live,rk_live,service_role,sb_secret,sk-andsk-ant-. - Your repository: if it is public on GitHub, search it for the same strings, and check its settings for GitHub’s secret scanning alerts.
Found one? The first ten minutes
- Revoke it first. In the provider’s dashboard (Stripe, Supabase, OpenAI…), roll or delete the key and create a new one. Deleting it from your code isn’t enough: old versions of your site, browser caches and anyone who already copied it still have it.
- Look for damage. Check the provider’s logs and billing for use you don’t recognise. For AI keys, set a monthly spending limit while you are there.
- Move it server-side. The new key goes into your backend’s secrets (a Supabase edge function, a Next.js API route), never into a variable the browser can read. The page calls your backend; your backend calls the provider.
- Deploy and check again. Run the free check once more on the live site.
A secret key was exposed in my app's browser code. I have already revoked it and created a new one.
1. Find every place the old key or its variable is used in code that runs in the browser, including variables with a VITE_ or NEXT_PUBLIC_ prefix.
2. Move each call that needs the secret into a backend function (a Supabase edge function, or an API route if this app has a server). The function reads the key from its secrets with Deno.env.get or process.env, and the page calls the function instead of the provider.
3. Check that the function only does what the page needs: the signed-in user's own data, a fixed model or price, sensible input limits. It must not become an open proxy for my key.
4. Remove the old variable from the front end entirely, and tell me which secret to add, under which name, in which dashboard. Don't paste any key into the code.
After the fix, searching the live site's files for the key's prefix must find nothing.Keep it from happening again
- Name secrets without the public prefix, so the build leaves them out of the code browsers download.
- Keep the GitHub repository private unless you mean it to be open.
- Run the check after every big change to how the app talks to outside services. Vigilia also re-checks your public code for secret keys every week and tells you if one appears, and recognises AI APIs being called straight from the browser.
Check your live app in ten seconds
The free launch check reads your live app the way a stranger would: leaked secret keys, the security certificate, browser security settings, link previews, phone layout and speed. No sign-up.
Read next
Questions
Is the Supabase anon key a secret?
No. It is meant to be in the browser, and Supabase’s security comes from row-level security policies on each table. The service-role key, or the newer secret key, is the one that must never reach the browser: it bypasses every policy.
Is it enough to delete the key from my code?
No. Old deploys, caches, a public git history and anyone who already copied it still have the key. Revoke it in the provider’s dashboard and create a new one first, then move the new one into your backend’s secrets.