Vigia › Help › Supabase service_role key exposed
Supabase service_role key exposed? What to do now
Short answer: Treat it as leaked. A Supabase secret key, or the legacy service_role key, "bypasses every Row Level Security policy you have", and anyone who loaded your site may have a copy. Fix the cause, create a new secret key in Settings → API Keys, replace the old key everywhere, confirm, then delete the leaked secret key or deactivate the legacy keys.
On this page: Why it matters · How it happens · How to check yourself · How to fix it · Prompts for Lovable, Bolt and Cursor · FAQ · How Vigia checks this · Sources
Why it matters
Supabase's docs: secret keys "provide full access to your project's data, bypassing Row Level Security". The service_role key is the legacy version of a secret key. "Never put one in a browser, a shipped application, or source control."
How it happens
- The key goes into a variable with a public prefix. Supabase's own
.envexample warns: "Server-only. Never prefix these, or your bundler will ship the key."VITE_andNEXT_PUBLIC_values end up in the JavaScript sent to the browser. - A query fails under RLS, and the quick fix is to swap in the key that ignores RLS.
- The key is committed to the repository or pasted into a client-side file.
How to check yourself
- Search the live site. Open DevTools → Sources and search all files for
sb_secret_. Any match in browser code is a leak. - Look for legacy keys. Legacy keys are long strings starting with
eyJ. Your anon key looks like that too, so you need to decode each one to see whether its role isservice_role. - Check your variables. No secret key should sit behind a
VITE_orNEXT_PUBLIC_prefix.
How to fix it
Supabase's procedure for a leaked secret key or a leaked service_role key:
- Fix the root cause first. "Fix the root cause of the leak before you rotate anything." Move every call that needs the key to server-side code (an API route or an Edge Function) and use only the publishable key in the browser.
- Create a new secret key in Settings → API Keys. If your project only has legacy keys, creating the new keys is safe: they work alongside the legacy keys.
- Replace the compromised key everywhere your application uses it. A leaked service_role key is replaced by the new secret key.
- Confirm every component now uses the new key, including the easy-to-miss ones: CI/CD, cron jobs, webhooks and app versions already in users' hands.
- Retire the leaked key. Delete a secret key (this can't be undone, so do step 4 first). For a legacy anon or service_role key, deactivate the legacy keys in the same section. That is reversible if you find a client you missed.
Note: new secret keys return HTTP 401 when used from a browser. Supabase is clear that this is not a fix: "an attacker can still use the key from other tools."
Prompts for Lovable, Bolt and Cursor
Find every place the Supabase secret or service_role key is used in client-side code or in a VITE_ or NEXT_PUBLIC_ variable. Move each of those calls to a server-side function (an API route or a Supabase Edge Function) that reads the key from a server-only environment variable and checks who is calling. Use only the publishable key in the browser. If a query only worked with the secret key, write the RLS policy it needs instead.
FAQ
I removed the key from my code. Am I safe now?
No. Anyone who loaded your site may already have it. Create a new key, replace it everywhere, and delete or deactivate the old one.
Can I just rotate the legacy service_role key?
Supabase's procedure replaces a leaked service_role key with a new secret key and then deactivates the legacy keys. Supabase is deprecating the anon and service_role keys by the end of 2026.
Will deactivating the legacy keys break my app?
Anything still using the legacy anon or service_role key will stop working, so move every client to the publishable and secret keys first. Deactivation is reversible.
New secret keys don't work in a browser. Doesn't that protect me?
No. The browser block returns HTTP 401, but a leaked key still works from other tools. Delete it.
Is my anon or publishable key a leak too?
No, it is meant to be public, as long as RLS is on for every table. See Supabase RLS disabled?
How Vigia checks this
Vigia is a read-only check for apps you own. It downloads your home page and up to 6 same-origin JavaScript files linked from it (<script src> and module preloads; scripts from a CDN or subdomain aren't checked). It also requests /.env, /.env.local, /.env.production, /.git/HEAD and /.git/config. It reports sb_secret_ keys and Supabase JWTs with the service_role role as critical, with a fix prompt for Cursor, Lovable or Bolt: keep it server-only and rotate it. It asks you to prove you own the site (a token file or meta tag) before it runs the check. Only the browser tab that proved it can run the check, for 24 hours; after that, you prove it again with a new token. It masks every value it shows (first and last 4 characters at most), including the start of an exposed .git file.
Honest limits: it does not run your app or log in, so keys in scripts that load later or behind a login can be missed. It can't tell whether a key is still active, so rotate it either way. A clean report is not a security guarantee.
The free check is available now. Vigia Pro (€12/month or €120/year, VAT included) re-checks up to 3 apps every week and emails you what changed. Run the free check
Sources
- Supabase: API keys (including "Rotate a leaked or compromised key")
- Supabase: Migrating to publishable and secret API keys
- Supabase: Row Level Security
- Vite: Env variables and modes
- Next.js: Environment variables
Supabase, Vite, Next.js, Lovable, Cursor and Bolt are trademarks of their respective owners. Vigia is not affiliated with or endorsed by them.