VibeRaven

Supabase service role key leaked or exposed: how do I rotate it?

Treat it as compromised and replace it: Supabase says a secret key "bypasses every Row Level Security policy you have", and the legacy service_role key has the same access, so whoever holds it can read and change all of your data. Fix where it leaked, create a new secret key in Settings, API Keys, switch every server component to it, confirm nothing uses the old key, then retire the old one: deactivate the legacy keys, or delete a leaked secret key, which cannot be undone. Removing the key from your code or git history does not undo the leak, because anyone who copied it can still use it.

Why this matters

The service role key is the one Supabase key that ignores row level security. It leaks from AI-built apps in a few repeated ways: a variable named NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY or VITE_SUPABASE_SERVICE_ROLE_KEY, which the build copies into the JavaScript it sends to the browser; a createClient call with the service role key in a client component; or a committed .env file. Supabase's own .env example keeps server keys unprefixed and says why: "Never prefix these, or your bundler will ship the key." Supabase also blocks a secret key sent from a browser with HTTP 401, but says an attacker "can still use the key from other tools".

What we do not know yet

  • Whether a new legacy service_role key can be issued on its own. Supabase's leaked key steps, as read on 2026-10-04, replace a leaked service_role key with a secret key and then deactivate the legacy keys; they give no step that issues a new legacy service_role key alone.

How to find the leak and rotate the key

  • Find the key without spreading it. Every key is in the dashboard under Settings, API Keys, legacy or not, and supabase projects api-keys --project-ref <your-project-ref> lists them from the CLI. Do not paste it into chat, a URL or a log: Supabase's rules for handling secret keys rule out all three.
  • Search the repo, env files included, for the key names and for a raw secret key:
    grep -rnE "service_role|SERVICE_ROLE|sb_secret_|SECRET_KEY" . --exclude-dir=node_modules --exclude-dir=.git
  • Any match behind a public prefix such as NEXT_PUBLIC_ or VITE_, or read in a file that starts with "use client", ships to the browser. Rename the variable without the prefix and read it only in server code: a route handler, a server action or an Edge Function.
  • Check the built client code, not only the source. A NEXT_PUBLIC_ value is inlined at build time, so the key stays in the deployed JavaScript until you rebuild. Next.js puts client code in .next/static and Vite in dist/assets. Legacy keys are JWTs, so the second command prints the role inside each one instead of the key: anon is expected, service_role is the leak.
    # Files in the client build that hold a secret key (prints file names, not keys)
    grep -rlE "sb_secret_[A-Za-z0-9_-]{8,}" .next/static dist/assets
    
    # The role inside each JWT in the client build
    grep -rhoE "eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+" .next/static dist/assets | sort -u \
      | node -e 'require("fs").readFileSync(0, "utf8").trim().split("\n").filter(Boolean).forEach((t) => console.log(JSON.parse(Buffer.from(t.split(".")[1], "base64url")).role))'
  • Check git history. A key deleted from the current files is still in every commit that had it, and in every clone and fork of a public repo. This lists the commits that added or removed a match, without printing the values:
    git log --all --oneline -E -G "sb_secret_|service_role|SERVICE_ROLE"
  • Create a new secret key in Settings, API Keys. Supabase suggests one secret key per backend component, so a later leak only means rotating that one.
  • Replace the leaked key with the new one everywhere your servers read it, including the environment variables in your hosting provider, and redeploy. Confirm every component now uses the new key.
  • Retire the old key. Delete a leaked secret key, which cannot be undone, so only after the previous step. For the legacy service_role key, deactivate the legacy keys in the same section, which you can reverse if you find a client you missed. That step turns off the legacy anon key as well, so move your frontend to the publishable key first.

When the service role key does not bypass RLS

The opposite complaint: a server client created with the service role key still gets RLS errors or empty results. Supabase's troubleshooting guide says RLS is enforced based on the Authorization header, not the apikey header, and a client created with the service role key "will ALWAYS bypass RLS" only while that header carries the key. These are the ways a user's token takes its place:

An SSR client created with the service role key

The SSR clients share the user session from cookies, so the user's token goes in the Authorization header and the request runs as that user. Create a separate client for service role work with supabase-js directly, with no session:

import { createClient } from '@supabase/supabase-js'

// Server only. Never import this file from client code.
export const supabaseAdmin = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SECRET_KEY!, {
  auth: { autoRefreshToken: false, persistSession: false },
})

An Authorization header set by hand

Edge Functions and other server code can set the Authorization header in the createClient options to the caller's JWT. A header you set overrides the key, so the request runs as the caller, under their policies.

Auth calls on the service role client

signUp and other auth functions can return a user session to the client that called them, and from then on that client sends the user's token. Create users with auth.admin.createUser() instead, or keep one client for auth calls and another for service role work.

A policy written for service_role

It does nothing: Supabase says service role "will never run the policies to begin with". If a service role request still fails, read the error: permission denied for table means a missing grant, which applies to service_role too.

Optional repo check

For AI-built Vercel + Supabase apps, npx -y viberaven@1.6.3 check reads the repo for the first two leaks above and names the file and line. In runs on 2026-10-04 and 2026-10-05 it flagged NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY in .env.example as service_role_key_in_client_env and a createClient call with the service role key in a "use client" file as service_role_key_in_client_code, both as blockers with a reminder to rotate the key. It also flagged VITE_SUPABASE_SERVICE_ROLE_KEY in .env.example, a createClient call reading it from import.meta.env in a Vite src file, and a hardcoded sb_secret_ value in a "use client" file. It cannot see your git history, your built bundle, the keys in your Supabase dashboard or whether a leaked key was already rotated. It is advice, not a gate, and a repository check, not a live database or security test. It exits with code 1 when it finds a blocker and writes a .viberaven folder that it does not add to .gitignore.

Related guides

Sources

Where do I find my Supabase service role key?

In the dashboard under Settings, API Keys, which lists every key, legacy or not. From the CLI, supabase projects api-keys --project-ref <your-project-ref> lists them. For a local stack, supabase start and supabase status print a secret key, which takes the place of the local service_role key.

Is the Supabase service role key deprecated?

Yes. Supabase is "deprecating the anon and service_role keys by the end of 2026" in favor of publishable and secret keys. A secret key has the same access, starts with sb_secret_, and can be deleted on its own without touching your other keys.

Can I use the service role key in an Edge Function?

Yes, that is server code. Supabase's docs suggest the @supabase/server SDK, which verifies the caller's secret key and hands back a privileged client, so the key never appears in your code. SUPABASE_SERVICE_ROLE_KEY still exists in the Edge Functions runtime, but it carries the legacy key.

I rewrote git history. Do I still need to rotate?

Yes. Rewriting history changes your copy and the remote you push to, not clones, forks or anything already copied, and Supabase says to delete a leaked key immediately. Rotate first, then clean up history if you want to.