Guides / Supabase service_role or secret key exposed in client code: what to do

last checked oct 2026

Supabase service_role or secret key exposed in client code: what to do

The publishable (or legacy anon) key is meant to be in your frontend. The secret key and the legacy service_role key are not. If either one reaches the browser, anyone who opens dev tools can use it to read and change everything in your database.

The short answer

Requests made with a Supabase secret key (sb_secret_…) or the legacy service_role key run as the service_role Postgres role. That role has the BYPASSRLS attribute, so none of your Row Level Security policies apply. If one of those keys is in your client bundle, treat it as leaked. First move the code that uses it to a server or Edge Function. Then create a new secret key, switch your backend to it, and delete or deactivate the old one.

Which key is which

The legacy keys are long JWTs that start with eyJ, and both look alike at a glance. The difference is in the payload. Decode the middle part (for example on your own machine, not in an online tool) and look for "role": "anon" versus "role": "service_role". Supabase says it is deprecating the legacy anon and service_role keys by the end of 2026, in favor of publishable and secret keys.

The newer secret keys have one built-in guard. Supabase checks the User-Agent and returns 401 when a secret key is used from a browser. That stops your own frontend from working with it, but it does not make a leak safe, because an attacker can use the same key from curl or a script.

How this ends up in the frontend

How to check

1. Search your source and env files

These commands print file names only, so the key itself never lands in your terminal history or logs:

# Any secret key or service_role reference in the code?
grep -rlE "sb_secret_|service_role|SERVICE_ROLE" \
  --exclude-dir=node_modules --exclude-dir=.git .

# Public-prefixed variables that look like server secrets
grep -rnE "^(NEXT_PUBLIC_|VITE_)[A-Z_]*(SERVICE|SECRET)" .env* 2>/dev/null | cut -d= -f1

2. Search the built bundle

What matters is what actually ships. Build the app and search the output folder (dist/ for Vite, .next/static/ for Next.js):

npm run build
grep -rlE "sb_secret_" dist/ .next/static/ 2>/dev/null

For the legacy service_role JWT, the string service_role only appears after decoding, so also check which env var names your client code reads. If createClient in any file under src/ or a client component reads a variable with “SERVICE” or “SECRET” in its name, that's your leak.

3. Check the live site

Open your deployed app, then go to DevTools → Sources (or Network → JS) and search all files for sb_secret_. You can also run our free Leak Checker on your own URL. It loads the page and its JavaScript the way a visitor's browser does and flags secret keys it finds, shown partly hidden.

4. Check git history

If the key was ever committed, it's still in history even after you delete the line:

git log --all --oneline -S "sb_secret_"
git log --all --oneline -S "SERVICE_ROLE"

How to fix it

Step 1: Move the privileged code to the server

Whatever the browser was doing with the secret key needs to happen in code you control, such as a Supabase Edge Function, a Next.js route handler or server action, or your own API. That code should check who the caller is before it does anything privileged. In the browser, go back to the publishable key:

// client: publishable key only
import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY
)
// server only: never give this variable a NEXT_PUBLIC_ or VITE_ prefix
import { createClient } from '@supabase/supabase-js'

export const supabaseAdmin = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_SECRET_KEY
)

If the frontend needed the service key because a query failed, the real problem is usually a missing policy. Write the RLS policy instead (see our USING (true) guide).

Step 2: Rotate, in this order

Supabase's leaked-key procedure is:

  1. Fix the root cause first (Step 1), so the new key doesn't leak the same way.
  2. Create a new secret key in Settings → API Keys.
  3. Replace the old key everywhere your backend uses it: hosting env vars, Edge Function secrets, CI.
  4. Confirm every component is on the new key.
  5. Retire the old one. A secret key is deleted, and that can't be undone. Legacy anon/service_role keys are deactivated as a pair in the same section, which you can reverse.

Deactivating the legacy keys turns off the legacy anon key too. Move your frontend to the publishable key before you deactivate, or the app will stop loading data.

Step 3: Clean up and look back

For the general “find and rotate any leaked key” playbook, see How to find and rotate exposed API keys.

next step

Check your live app for free

Our free Leak Checker looks at your deployed app the way any visitor's browser does and flags exposed secret keys, public source maps and missing security headers. Paste your app's URL and see whether a secret key is sitting in the JavaScript you ship.

If you'd rather have the whole app looked at, the $99 Scan + Report covers your repo and live app, including Supabase RLS, leaked keys and auth checks.

Sources

Every product step above was checked against these official pages. Last checked Oct 2026. Products change; if a menu moved, the docs below are the source of truth.