Supabase service role key in browser: find it and fix it
A Supabase service role key in browser DevTools is a master key. It bypasses Row Level Security, so anyone who reads your JavaScript can read and write every row. This guide shows how to spot that key in a built bundle, rotate it, and leave only the anon or publishable key in client code.
QUICK CHECKLIST
- Search the built JS for service_role and sb_secret_
- Confirm the client uses only anon or publishable
- Move elevated keys to a server route or Edge Function
- Rotate any elevated key that ever shipped
- Re-test tables with the anon key while logged out
- Remove NEXT_PUBLIC_ or VITE_ prefixes from secret env names
Why a Supabase service role key in browser is a full takeover
Supabase gives you two privilege levels. The publishable key (and the legacy anon key) is meant to ship in browsers, mobile apps, and CLIs. It only reaches what Row Level Security allows.
The secret key (and the legacy service_role key) is for servers, Edge Functions, and other backends you control. Those keys bypass Row Level Security. Supabase's own docs put it plainly: they provide full access to your project's data.
So if createClient in a React or Next.js client file gets the service_role or secret value, every visitor can act as that elevated role. RLS policies you wrote for normal users never run for that client.
| Key | Format | OK in browser? | Bypasses RLS? |
|---|---|---|---|
| Publishable | sb_publishable_... | Yes | No |
| anon (legacy) | long JWT | Yes | No |
| Secret | sb_secret_... | No | Yes |
| service_role (legacy) | long JWT | No | Yes |
From Supabase API keys docs. Secret and service_role bypass RLS.
How the key usually gets into the frontend
AI coding tools often "fix" a failing browser query by swapping in the service_role key, or by putting SUPABASE_SERVICE_ROLE_KEY behind NEXT_PUBLIC_ or VITE_ so the client can read it. Next.js inlines any NEXT_PUBLIC_ value into the browser bundle. Vite does the same for VITE_.
Shared helpers make it worse. A lib/supabase.ts that exports both a browser client and an admin client gets pulled into client components, and the bundler can ship the elevated key even when you thought it was server-only.
For the general rule that secrets cannot live in frontend env vars, see hide API keys in React and is the anon key safe.
Find a Supabase service role key in your browser bundle
Run these checks on a production or preview build of your own app:
- Open the site, DevTools → Sources (or Network → a large .js file) and search for
service_role,sb_secret_, androle":"service_role. - Search your repo and env examples for
SERVICE_ROLE,SECRET_KEY, and any of those names with aNEXT_PUBLIC_orVITE_prefix. - Decode a JWT that starts with
eyJ(legacy keys only): if the payload role isservice_role, that key is elevated. Publishable and secret keys are short strings, not JWTs. - In the Network tab, call your own Supabase REST URL while logged out. If you can select rows that policies should hide, you either have RLS off or you are not using the anon/publishable key.
Before and after: the client and the server
const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY! )
const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! )
// lib/supabase.ts imported by both client and server export const admin = createClient(url, process.env.SUPABASE_SERVICE_ROLE_KEY!)
// app/api/admin/route.ts (server only)
import { createClient } from '@supabase/supabase-js'
const admin = createClient(url, process.env.SUPABASE_SERVICE_ROLE_KEY!)On the server, create a separate admin client only where you need elevated access, and never import that module from a Client Component. For user-scoped server work, prefer a client that forwards the user's Authorization header so RLS still applies.
Fix a Supabase service role key in browser, step by step
Do this on your own project. Create the replacement key before you revoke the old one if the app is live.
Inventory every elevated key
List Dashboard API keys: legacy service_role and any sb_secret_ keys. Note which host env vars, Edge Function secrets, and CI secrets hold them.
Confirm what the browser actually ships
Build the app. Search the output for service_role and sb_secret_. Any hit means the key is public.
Switch the browser client to anon or publishable
In every client createClient call, use the publishable key or legacy anon key. Keep the name behind NEXT_PUBLIC_ or VITE_ only for that low-privilege value.
Move elevated calls to the server
Admin inserts, bypasses, and webhooks belong in a Route Handler, Server Action, or Edge Function that reads SUPABASE_SERVICE_ROLE_KEY or a secret key from server env. No public prefix.
Redeploy and re-search the bundle
Deploy the fix. Search the new JS again. Zero hits for service_role and sb_secret_.
Rotate the leaked key
In the Dashboard, create a new secret key (or follow Supabase's rotate guidance for a leaked service_role). Update every server and Edge Function. Confirm they work. Then delete or deactivate the compromised key. Do not revoke first if production still needs it.
Prove RLS still holds
With only the anon or publishable key, call /rest/v1 on your tables while logged out, then as a second user. Follow test Supabase RLS if you want the three-request script.
If you also committed the key to git, rotate first, then clean history when you can. Rotation is what stops the bleeding.
Fix prompt you can paste
Find Supabase service_role or secret keys that can reach the browser. Do not print any key values. 1. Search the repo, including .env* files and every NEXT_PUBLIC_ or VITE_ variable, for service_role, sb_secret_ and SERVICE_ROLE. 2. List every createClient call: file, line, which key it uses (anon/publishable or service_role/secret), browser or server. 3. Rewrite browser code to use only the anon or publishable key. 4. Move any client that needs the secret key into a server-only file and read the key from a variable without a public prefix. 5. Explain each change and list anything you were unsure about.
Further reading
- Supabase: API keys
- Supabase: Securing your API
- Supabase: Row Level Security
- Supabase: Creating a server client
- Next.js: Environment variables
FAQ
Is the anon key safe in the browser?
Yes, if every table has RLS on with real policies. The anon and publishable keys are designed to be public. The danger is the service_role or secret key, or RLS left off.
Do the new sb_secret_ keys bypass RLS too?
Yes. Secret keys have elevated privileges and bypass Row Level Security. Keep them on backends you control, same rule as service_role.
I already shipped the key. Is deleting the line enough?
No. Anyone who saved the bundle or cloned the repo still has it. Create a new secret key, update servers, then deactivate or delete the old one.
Can I put the secret key in a Server Component file that also imports a client helper?
Only if that elevated client never ends up in a Client Component import graph. Prefer a dedicated server-only module and a separate browser client module.
How do I test after the fix?
Search the new bundle for service_role and sb_secret_. Then call your REST API with only the anon or publishable key while logged out and as a second user.
Want a second pair of eyes?
We review your whole repo and write a fix prompt for every finding. $19, report within an hour.
Check my repoLeakcade is an automated check. It can miss vulnerabilities and is not a substitute for a professional security audit or pentest. Provided as is. You stay responsible for your code. Where the law allows, our liability is limited to what you paid ($19). Terms