My Supabase service_role key is exposed
This is the worst key you own, and it is also the one most likely to end up committed โ because it's the one that makes your migration script work. Here's what to do, in order.
Do this first
- Confirm it's actually the service_role key and not the anon key. They look identical and only one is an emergency. Thirty seconds, below.
- Rotate it in the Supabase dashboard, under Settings โ API.
- Then look at your data, not just your billing. This is the part people skip.
1. Which key is it? Decode it and see
Supabase gives you two keys that look the same โ long, three-part strings
starting eyJ. One is public by design and one is catastrophic.
Before you do anything else, find out which one you're holding.
They're JWTs, which means they aren't encrypted, just encoded. Paste the middle section (between the two dots) into any base64 decoder, or run:
echo 'PASTE_THE_MIDDLE_SECTION' | base64 -d
You'll get something like {"iss":"supabase","role":"anon",...}
or {"role":"service_role",...}. That word is the whole answer:
| Role | What it means |
|---|---|
anon |
Not an emergency. This key is meant to be public โ it's in your page source and always has been. It only does what your Row Level Security policies allow. If this is the one that leaked, you have not lost anything. Go and check your RLS policies are actually written, then get on with your day. |
service_role |
Emergency. Keep reading. |
Newer Supabase projects use a different format โ
sb_publishable_โฆ and sb_secret_โฆ โ where the name
tells you directly. Same distinction, less decoding.
2. Why this one is different from other leaked keys
A leaked OpenAI key costs you money. A leaked service_role key costs you your data, and the difference matters for what you do next.
The service_role key bypasses Row Level Security entirely.
Not "has broad permissions" โ bypasses. Every policy you carefully wrote,
every auth.uid() = user_id check, stops applying. Anyone holding
that key can read, edit and delete every row in every table in your project,
from their own laptop, without touching your app.
That's why rotating isn't the whole job. With most leaked keys, the question is "what did it cost me". With this one, the question is "what did they see, and did they change anything" โ and the answer isn't in your billing page.
3. Rotating it โ and the thing that catches people
In the dashboard: Settings โ API. Newer projects can revoke and reissue the secret key on its own, which is clean and uneventful.
Older projects, using the JWT-based keys, have a catch worth knowing before you click:
Regenerating the JWT secret invalidates both keys and signs every user out. The anon key changes too โ so any deployed frontend still holding the old one stops working until you redeploy with the new value โ and existing sessions become invalid, so all your logged-in users get logged out at once.
That's survivable, and it's much better than leaving the key live. But do it knowing it will happen, rather than discovering it from a support message ten minutes later. Have the new anon key ready to deploy before you rotate.
Deleting the key from the file is not rotation. It stays in your git history, and public repositories are crawled for credentials within minutes of a push โ frequently faster than you'll finish reading this page.
4. What to actually check afterwards
Because the key bypasses RLS rather than using it, ordinary application logs won't show the access โ it didn't go through your app. Look at the data itself:
- Rows you didn't create. Sort your main tables by
created_atdescending and look for entries with no matching application activity. Injected admin users are the classic one โ check any roles or permissions table first. - Rows that changed without a reason. If you have
updated_atcolumns, the same trick. - Missing rows. Harder to spot, which is why a backup from before the exposure window is worth pulling now while you still know the dates.
- Storage buckets. The service_role key reaches those too, and people forget them because they think in tables.
If your project holds other people's personal data and you find evidence of access, that may be a reportable breach where you are. That's a question for someone who knows your jurisdiction, and it's a much easier question to answer while the dates are fresh.
5. Where it usually leaked from
Scanning a couple of hundred public AI-built apps, the service_role key was the most common serious finding by a distance โ and it was almost never in the application code. It was in the supporting cast:
- One-off migration and seed scripts.
run-migration.mjs,setup-database.js,add-column.ts. These need to bypass RLS, which is exactly why they get the powerful key โ and they're written in a hurry and never reviewed. - Deployment notes.
DEPLOYMENT.md,VERCEL_ENV_VARS.md,DEPLOY_OPTIONS.md. Someone pastes the real environment variables into a markdown file so they can copy them into a dashboard later, and the file gets committed. Markdown doesn't feel like code, so it doesn't feel like a leak. - Platform config files.
.replit, deploy scripts, CI files.
Notice what those have in common: none of them is the app. If you're
checking whether you've done this, grep the whole repository, not just
src/.
6. Keeping it out next time
The service_role key belongs in exactly two places: a server-side environment variable, and an edge function's secrets. Nowhere else, ever.
- Key read from
process.env, never a literal - Never in a
VITE_/NEXT_PUBLIC_/REACT_APP_variable โ those compile into the browser bundle - Scripts that need it read it from the environment too, not from a constant at the top
.envin.gitignore, and check it's actually covered- Row Level Security enabled and scoped on every table, so the anon key alone can't do damage
- GitHub push protection on, so the next one gets stopped before it lands
Paste this into whatever built the app:
Find every place my Supabase service_role key appears in this repository, including markdown files, deployment notes, scripts and config files. Replace each with a read from an environment variable named SUPABASE_SERVICE_ROLE_KEY, move any client-side usage into a server-side route or edge function, and list every file you changed so I can confirm nothing was missed.
Check whether there's another one
vibecheck scans your project for exposed keys โ decoding Supabase JWTs to tell a harmless anon key from a service_role one โ and tells you in plain English, with a fix prompt to paste back into your AI coding tool.
Scan my project โ free