AppPulse

Guides

Supabase service_role key exposed: what to do in the next 10 minutes

A Supabase service_role JWT or sb_secret key in client code bypasses row-level security. Rotate that key, remove it from the bundle, and redeploy.

A Supabase service_role key bypasses row-level security. So does a secret key that starts with sb_secret_. If either value was in a JavaScript bundle, a public repo, a screenshot, or a chat someone else could open, treat it as copied. Rotating is the fix. Editing the file and hoping nobody saved it is not.

Confirm you are looking at the service key and not the anon key. The two-minute bundle test shows how to decode the JWT locally. A payload role of anon is not this emergency.

Minutes 0–2: name the key

Minutes 2–6: rotate the leaked key

Open that project in the Supabase dashboard, then Project Settings and the API or API Keys section.

Update the server environment that is allowed to hold the new secret: a host env var, a Supabase edge function secret, or a backend that is not shipped to the browser. Do not put the new value in a VITE_ or NEXT_PUBLIC_ variable.

Minutes 6–8: get the old key out of the client

Paste this into the coding tool, then redeploy.
Search the repository and the client bundle for service_role and for sb_secret_.
Delete those strings from every client component and from every VITE_ or NEXT_PUBLIC_ variable.
The browser may keep the anon JWT or a publishable key.
The new service secret belongs only in server-side env that is not included in the built JavaScript.
Redeploy the public site so the old bundle is replaced.

After deploy, repeat the search on the live URL. A cached CDN copy of the old script can keep serving the key for a while. Purge that cache if the host lets you, then search again.

Minutes 8–10: look for use, then lock tables

In Supabase, open the API logs for the project and look for a burst of requests you do not recognize, especially from the service role. Check Authentication for new users you did not create, and Storage for new public objects. This page cannot see those logs for you.

Then turn on row-level security for private tables. The service role was ignoring those policies; the anon key, which stays in the browser, will not. Use the RLS checker for the SQL and the logged-out GET.

What the free scan actually checks

The scan reads the public homepage and up to six scripts linked from it. It reports a critical finding for an sb_secret_ value, and for a JWT whose role is service_role when the issuer contains “supabase” or the payload includes a project ref. It redacts the value in the report. It does not call your database with that key.

It also GETs a short list of common files, including /.env, /.env.local, and /.env.production, and flags them only when the body looks like that file rather than your homepage HTML. It will not see a key that lives only in a private env var, a git history that is not being served, or a script the homepage does not link.

Questions

Does rotating the JWT secret also change the anon key?

Yes. Supabase signs the legacy anon JWT and the service_role JWT with the same secret. Rotating that secret replaces both and signs users out. Revoking a key that starts with sb_secret_ does not, by itself, replace the anon key.

Will AppPulse use a leaked service_role key to read my database?

No. If the homepage or a linked script contains a Supabase service_role JWT or an sb_secret_ value, the scan reports it and does not send that key to your project.

The key was only in a chat, not in the live site. Do I still rotate it?

Yes, if anyone else could see that chat, a screenshot, or a commit. The free scan only sees files the public site serves. It will not clear a key that never made it into those files.

Other guides

Run it against your live URL

The free scan reads the public site only. No account, no writes, and no use of a service key if one is sitting in the bundle.

Scan your URL