Guides / Supabase service_role or secret key exposed in client code: what to do
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
- Publishable key (
sb_publishable_…), or the legacyanonJWT: safe in the browser, and only reaches what RLS allows. - Secret key (
sb_secret_…), or the legacyservice_roleJWT: server only, and bypasses RLS.
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
- The key is stored in a variable with a public prefix like
NEXT_PUBLIC_(Next.js) orVITE_(Vite, which Lovable and most Bolt projects use). Bundlers inline these into the JavaScript that ships to every visitor. - An AI assistant “fixed” a permission error by swapping the publishable key for the service key in
createClient. - An admin page or script that runs in the browser was given the secret key so it could skip RLS.
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:
- Fix the root cause first (Step 1), so the new key doesn't leak the same way.
- Create a new secret key in Settings → API Keys.
- Replace the old key everywhere your backend uses it: hosting env vars, Edge Function secrets, CI.
- Confirm every component is on the new key.
- Retire the old one. A secret key is deleted, and that can't be undone. Legacy
anon/service_rolekeys 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
- Remove the key from the repo and any
.envfile that's committed. If the repo is public, assume the key was copied, and rotating is the real fix. Rewriting history alone doesn't undo a leak. - Look over your data for rows or users you don't recognize, and check your project's API logs for the period the key was public.
- Use a separate secret key for each backend component, so one leak only means one rotation.
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.