Supabase service_role key exposed? Do this in the next 15 minutes
By Pritesh Ghodge, security engineer · Last updated 23 October 2026
If your Supabase service_role key (or a newer sb_secret_ key) is sitting in your site's JavaScript, a public repo or a leaked env file, treat it as compromised. This is the one Supabase key that matters most. Here's what to do, in order.
Why this key is different
The anon (or sb_publishable_) key in your front end is meant to be public. Row Level Security (RLS) decides what it can see.
The service_role key and sb_secret_ keys skip RLS completely. Anyone holding one can read, change or delete every row in your database, whatever your policies say. They're meant for servers and edge functions only.
The usual way it ends up in the browser: an AI agent hits a "permission denied" error from RLS while building, swaps in the service_role key to make it go away, and puts it in a VITE_ or NEXT_PUBLIC_ variable. Anything with those prefixes is built into the JavaScript every visitor downloads.
First, confirm what you're looking at
Open your live site, then DevTools → Sources, and search all files (Ctrl+Shift+F, or Cmd+Option+F on a Mac) for service_role, sb_secret_ and eyJ.
sb_secret_…: a secret key. Leaked.sb_publishable_…: public by design. Fine.- A long string starting
eyJ: a legacy JWT key. Decode it (any JWT decoder, or split on the dots and base64-decode the middle part) and read therolefield."anon"is fine."service_role"is the leak.
Also check .js.map files if your site serves them: source maps can contain the original code, keys included.
Step 1: Replace the key without breaking your app
Supabase's own procedure for a leaked secret or service_role key is: create the replacement, switch everything over, confirm, then retire the old one. In practice:
- Create a new secret key in the Supabase dashboard under Settings → API Keys. On older projects you may first see a button to create the new publishable and secret keys. That's safe: it adds them next to your legacy keys without turning anything off.
- Remove the key from the front end. Move the code that needed it to the server (Step 3) and give that server code the new secret key.
- If your front end uses the legacy anon key, switch it to the publishable key in the same change. When you retire a leaked legacy service_role key, the legacy keys are deactivated together, so anything still on the old anon key would stop working.
- Deploy and check that sign-in, reads and writes still work, and that the old key no longer appears anywhere in your live JavaScript.
- Retire the old key. Delete a leaked
sb_secret_key (this can't be undone). For a legacy service_role key, deactivate the legacy keys in the same dashboard section. Deactivation can be reversed if you find a client you missed.
If the database holds sensitive data and you can't ship a fix quickly, retire the key now and accept some downtime. A broken page for an hour costs less than someone quietly copying your users table.
Step 2: Check whether anyone used it
Nobody can tell you for certain whether a public key was copied. You can look for signs it was used:
- API logs. In the dashboard, open Logs and look at API requests during the window the key was public: unfamiliar IPs or user agents, bulk reads of whole tables, deletes and updates you didn't make. Log history is limited, especially on lower plans, so look now.
- Your data. Look for rows you didn't create, changed prices or roles, emptied tables, and new users in Authentication that you don't recognise.
- Storage. The key can read and delete files in every bucket, private ones included. Check for missing or unexpected files.
- Other secrets. If the key leaked through a public
.envfile, a repo or a hosting provider, everything stored next to it leaked too. Rotate those as well, starting with anything that can spend money (Stripe, OpenAI, email or SMS keys).
If you find signs that personal data was accessed, you may have to tell your users, and in some countries a regulator. Check what applies where you and your users are.
Step 3: Move privileged code to the server
Most apps don't need the service_role key in the request path at all. If it was added to get past an RLS error, the real fix is a policy that allows the action for the right user. See how to check and fix your RLS policies.
When you do need it (admin tasks, webhooks, background jobs), keep it in server-only code.
Next.js: keep it in an env var without the NEXT_PUBLIC_ prefix, and only read it in a Route Handler, Server Action or other server file. Importing server-only makes the build fail if a client component ever imports that file:
// lib/supabase-admin.ts
import "server-only";
import { createClient } from "@supabase/supabase-js";
export const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SECRET_KEY! // no NEXT_PUBLIC_ prefix
);Check every request that uses it on the server: who is signed in, and whether they're allowed to do this. The key itself skips all of those checks.
Lovable, Bolt and other Vite apps: there's no server in the browser bundle, so privileged calls belong in a Supabase Edge Function, with the key stored as a function secret. A prompt you can paste:
Security fix: the Supabase service_role / secret key must never reach the browser.
1. Remove every use of it from client code and from any VITE_ variable.
2. For each place that used it, either fix the RLS policy so the normal
client can do this for the signed-in user, or move the call into an
Edge Function that checks the user's session and reads the key from
a function secret.
3. Show me the diff and the list of places you changed before applying.Step 4: Clean up git and redeploy
If the key was ever committed, it's in your git history even after you delete it. On a public repo, assume it's been seen. Replacing the key (Step 1) is what actually fixes this; rewriting history is optional tidying. To find old secrets, run a scanner such as gitleaks over the whole history:
gitleaks git . -vThen add .env* to .gitignore, redeploy, and search your live JavaScript once more to confirm the old key is gone.
Stop it happening again
- Use the new
sb_secret_keys rather than the legacy service_role JWT. Supabase says secret keys return HTTP 401 when used from a browser, and you can create one per service so a single leak only forces one replacement. - Never give a secret a
VITE_orNEXT_PUBLIC_prefix. - Check the live site after every deploy, not just the repo. AI edits are how these keys get in, and they arrive with a deploy.
Abbi Labs does that last part for you. On domains you've verified you own, it scans your home page, its JavaScript files and any public source maps for Supabase service_role and sb_secret_ keys and other secrets (Stripe, OpenAI, Anthropic, AWS, GitHub and more). It also probes common tables with your public key and checks for exposed .env and .git files. The first scan is free, and Monitor re-scans weekly and after every deploy.
FAQ
Is my Supabase anon key a leak too?
No. The anon key and the newer sb_publishable_ key are designed to be public and are expected in your front-end code. Row Level Security decides what they can read. Only the service_role key and sb_secret_ keys bypass RLS and must stay on the server.
How do I tell if the key in my bundle is anon or service_role?
New keys say what they are: sb_publishable_ is public, sb_secret_ is secret. Legacy keys are long JWTs starting with eyJ. Paste one into a JWT decoder and read the role field in the payload: "anon" is fine, "service_role" is the one to replace.
I deleted the key from my code. Am I safe now?
No. If it was ever deployed or pushed to a public repo, assume someone copied it. Deleting it from the code doesn't revoke it. Only retiring the key in Supabase does.
Will replacing the key break my app?
It can, if something still uses the old key when you retire it. Create the new key first, switch every server-side use over, deploy, confirm it works, and only then retire the old one. Legacy keys are deactivated rather than deleted, so you can switch them back on if you missed something.
Does Abbi Labs check for this?
Yes. On domains you've verified you own, it reads your home page, the same-origin JavaScript files it links to and any public source maps, decodes any Supabase JWTs it finds, and flags service_role keys and sb_secret_ keys as critical. Reports show only a masked version of the key.
Related guides
- OpenAI, Claude or Gemini API key exposed in your frontend? What to do now
- Lovable app security checklist (from a security engineer)
- How to check if your Supabase tables are readable by anyone
- NEXT_PUBLIC_ and VITE_ variables are shipped to every visitor
- 3 files your production site shouldn't be serving: .env, .git and source maps
- Security headers for Lovable, Bolt, v0 and Cursor apps: CSP, HSTS and clickjacking
- Supabase "42501 permission denied for table": fix it with a GRANT, not by turning off RLS