Security basics
How to rotate a leaked Supabase service_role key
Supabase says fix the leak first. Other guides say rotate now. Which is right depends on where your service_role key leaked.

In short
- If your Supabase service_role key is in the JavaScript a browser downloads, rotate it before you fix anything. The copy already out there does not expire.
- If it only reached a private repo or a log, fix the source first, or your new key follows the old one out on the next deploy.
- A legacy service_role key cannot be rotated at all. Invalidating one means creating the new sb_secret_ keys, moving your app onto them, and disabling the old pair with a switch that covers both.
Somebody has told you that your Supabase service_role key is sitting in your
app where anyone can read it. Before you change anything, be certain that is the
key they found: our free scan reads your live site the way a stranger
would and tells you which Supabase key it can actually see there, with no
account and nothing to install.
The advice that follows a finding like that is almost always one word: rotate. Then you go looking for how, and the sources disagree. Supabase's own documentation opens its rotation guide by telling you to fix the root cause of the leak before you begin. Half a dozen third-party guides tell you to rotate immediately and ask questions afterwards.
Here is the part neither of them writes down: both are correct about different situations, and the question that separates them is where the key leaked to.
One thing to have straight before any of it. Deleting the key from your code takes your own copy off the ring. Rotating is changing the lock. Only the second one reaches the copies other people already have.
Is it really the service_role key?
In a newer project the first few characters answer it. sb_secret_ at the start
is the secret one. sb_publishable_ is the key that is supposed to be in your
app, and finding it there is not a finding at all.
Be sure before you do anything disruptive, because the key found in a vibe-coded app is usually the one that belongs there.
Older projects issue a different pair, anon and service_role, and those two
look almost identical: same format, same length, no prefix to read. The middle
section of one of those keys is not encrypted at all. It decodes to a few lines
of plain text, and one of them states the role.
Which API keys are safe in your frontend
walks through both checks.
The reason to be sure is that this is rarer than the warnings suggest. We
scanned 30,998 live vibe-coded apps in August 2026 and found a service_role
key shipped in 3 of them. A Google API key, which is usually harmless and often
locked to one domain, turned up in 1,142.
The full count is here.
Rotate first, or fix the leak first?
It depends on whether the key is already public, and the line between the two cases is clean.
If the key is in the JavaScript your visitors download, it is public now. Anybody who loaded your site while it was live has a copy, and so does every automated crawler that went looking for exactly that string. Nothing you change in your code reaches those copies. Rotate first, then fix the source.
If it only reached a private repository, a log file, a CI variable or a chat message, the exposure is bounded. Rotate first here and you will be doing it twice, because the next deploy pushes the old value back out of whatever produced it and your new key follows the old one out. Fix the source, then rotate.
Supabase's guide is written for the second case. The urgent guides are written for the first. If you are reading this because a scanner found the key on your live URL, you are in the first case.
How to rotate a Supabase secret key
If your project has sb_secret_ keys, this is a dashboard job with no downtime.
A project can hold more than one secret key at a time, each with its own name, and that is what makes it safe: you add the new one before you take the old one away, so nothing is broken in between.
- Open your project, go to Settings → API Keys and create a new secret key.
- Put it everywhere the old one was used, all of which should be on a server: edge functions, webhooks, scheduled jobs, a backend of your own. Where those values live if you have not opened that page before.
- Deploy, then click through the parts of your app that read and write data. A wrong secret key fails loudly and immediately, which is the good case.
- Only then delete the compromised key.
Step 4 is permanent. Supabase deletes a secret key outright: no undo, no re-enable, no parked copy you can bring back. That is why it goes last.
Deleting a secret key does not sign your users out. An API key says which application is calling your database, and a visitor's session token says which user they are. The second is checked against your project's signing key, and that is a separate value you have not touched.
Why a legacy Supabase service_role key cannot be rotated
Because there is no button for it. Supabase's troubleshooting note says direct
rotation of the legacy anon, service_role and JWT secrets is no longer
supported, and points you at the new keys instead.
So invalidating a leaked legacy service_role key is a migration rather than a
rotation:
- Create the new keys. This adds
sb_publishable_andsb_secret_alongside the old pair. Both systems work at once and nothing breaks. - Move your frontend onto the publishable key. In Lovable or Bolt this is usually one value in your project settings rather than a line of code.
- Move every server-side use onto the secret key.
- Disable the legacy keys in your project settings.
Step 4 is one switch and it covers both old keys. anon and service_role are
JWTs signed by the same secret, so the switch that revokes one revokes the
other, and a frontend still carrying the old anon key stops working the moment
you flip it. That is why step 2 comes before it.
Deactivation is reversible, which is the one piece of good news here: if something you forgot turns out to still be using a legacy key, you can switch them back on while you fix it. What the migration involves has the longer version.
Where the key leaked from, and how to close it
Three causes account for nearly all of it, and all three are inside your own project.
A VITE_ or NEXT_PUBLIC_ prefix. These are not a security setting
somebody forgot to switch on. They are an instruction to the build: put this
value in the bundle. A variable named VITE_SUPABASE_SERVICE_ROLE_KEY was
compiled into your JavaScript on purpose, by a tool doing exactly what it was
told to do.
A value pasted straight into a component. No prefix involved and no .env
file, just the key sitting in a line of code because that was the quickest way
to make a query return something.
Work that belongs on a server. Deleting an account, writing to a table your users are not allowed to write to, reading rows across everybody. The key got reached for because the browser could not do the job, and the fix is to move the job: an edge function, a serverless route, anything your visitors do not download.
How do I know if someone used it?
Usually you cannot be certain. What you can do is narrow the window and look inside it.
The window opens with the deploy that first shipped the key and closes when you disabled it. Your Supabase project keeps logs for both the API and the database, and that date range is what to filter them to. What you are looking for is reads and writes you cannot account for: requests to tables your app never touches, traffic at hours nobody was using it, deletes nobody made.
Then check the data itself. Row counts against what you expect, your own account records, anything with a timestamp that moved while nobody was working. A support message about data that changed on its own is how most of these are actually discovered, and it arrives weeks later.
A service_role key does not reach your payment provider or your mail sender.
It is still worth knowing whether it was the only key in your bundle, because
the ones that spend money leak the same way:
what 30,998 apps were shipping.
Check your live app before you call it done
Load your site in a private window, open the JavaScript your browser downloaded, and search it for the old value. Then search for the new one, which should not be there either.
Two things make this worth checking by hand. A build can serve a cached bundle for a while after you deploy, so the file a visitor gets is not always the file you just built. And a key can be in more than one place: a second entry point, a service worker, an old build still being served from a path nobody links to.
Our free scan does this part from outside, on the URL your visitors actually use, and it decodes a Supabase key far enough to read the role, so a publishable key comes back as a tick. It takes about twenty seconds and needs no account: scan your app.
What to do now
What to do
- Confirm the key first.
sb_secret_at the front, or"role": "service_role"inside an older one. A publishable key in your frontend is not a finding. - If it is in the bundle your visitors download, rotate before you change any code. Editing your app does not reach the copies already out there.
- If it only leaked to a repo, a log or a chat, fix the source first, so your next deploy does not push the new key straight back out.
- On a newer project: create a second secret key, move your server code onto it, deploy, then delete the old one. Deleting is permanent.
- On a legacy project there is no rotate button. Create the new keys, move your frontend and your server onto them, then disable the old pair with the one switch that covers both.
- Search your live bundle afterwards. A cached build can serve the old value for a while after you deploy.
A service_role key reads and writes every row in your database. The worst
version of this story ends with an empty table, and whether those rows come back
depends on
what you were backing up
before any of it happened.
FAQ
Does rotating the key break my live app?
Not if you do it in order. On a project with the newer keys you add a second secret key, move your server code onto it, deploy, and only then delete the old one, so there is never a moment with no working key. On a legacy project the risk is a different one: disabling the old pair also disables the anon key your frontend uses, so your app breaks unless it is already on the publishable key when you flip that switch.
Can I rotate the legacy anon and service_role keys?
No. Supabase's troubleshooting documentation says direct rotation of the legacy anon, service_role and JWT secrets is no longer supported. Invalidating a leaked legacy key means creating the new sb_publishable_ and sb_secret_ keys, moving your app and your server code onto them, and then disabling the old pair in your project settings.
Should I delete the old key or deactivate it?
You do not get to choose, because the two key systems behave differently. A newer sb_secret_ key is deleted, permanently, with no way to bring it back. The legacy pair is deactivated instead, and deactivation is reversible: if something you forgot turns out to still be using an old key, you can switch them back on while you fix it.
How do I know if someone used it?
Usually you cannot be certain. Narrow it to a window instead: the key was usable from the deploy that first shipped it until the moment you disabled it. Filter your Supabase API and database logs to that range and look for reads and writes you cannot account for, then check your data for row counts and timestamps that do not match anything you did.
Do I need to rotate my anon key too?
On a legacy project you do not get a choice: disabling the old pair takes both keys at once, so your frontend needs the new publishable key before you flip the switch. On a newer project the publishable key is meant to be public and there is no reason to touch it. What is worth checking either way is Row Level Security, because that is the only thing standing between a publishable key and your whole database.