Skip to content

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.

Vlad Tkachenko9 min read
A padlock with two identical keys beside it, drawn at the same size, one bright and one faded.

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.

Same two jobs, opposite order. What decides it is whether the leaked copy is already out in the world.

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.

  1. Open your project, go to Settings → API Keys and create a new secret key.
  2. 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.
  3. 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.
  4. 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:

  1. Create the new keys. This adds sb_publishable_ and sb_secret_ alongside the old pair. Both systems work at once and nothing breaks.
  2. 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.
  3. Move every server-side use onto the secret key.
  4. 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.

Disabling the legacy keys is one control for both of them. The anon key your app is using goes out with the service_role key you are trying to invalidate.

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.

Written by

Vlad Tkachenko

Founder, Reeve

I spend my time looking at apps built with Lovable, Bolt, v0, Cursor and Replit, and at the short list of mistakes that keep turning up in them.

More about the author

Read next

All articles

Not sure where your own app stands?

Run a free scan and get a plain-language grade from A to F in about 20 seconds. No account, no card.

Scan your app free

Automated external check, not a full audit. Absence of findings is not a guarantee of safety.