LEAKCADE

Supabase service role key in browser: find it and fix it

Updated 5 min read

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.

Wrong key vs right key in the browserTop lane uses service_role in the client and skips RLS. Bottom lane uses anon or publishable and hits policies.WRONGCLIENTcreateClient withservice_roleSKIPRLS never runsOWNEDattacker reads every rowRIGHTCLIENTanon or publishable onlyRLSpolicies decide accessSERVERsecret key server-sideWRONGCLIENTcreateClient with service_roleSKIPRLS never runsOWNEDattacker reads every rowRIGHTCLIENTanon or publishable onlyRLSpolicies decide accessSERVERsecret key server-side
Secret and service_role keys must stay on a server you control.
KeyFormatOK in browser?Bypasses RLS?
Publishablesb_publishable_...YesNo
anon (legacy)long JWTYesNo
Secretsb_secret_...NoYes
service_role (legacy)long JWTNoYes

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:

  1. Open the site, DevTools → Sources (or Network → a large .js file) and search for service_role, sb_secret_, and role":"service_role.
  2. Search your repo and env examples for SERVICE_ROLE, SECRET_KEY, and any of those names with a NEXT_PUBLIC_ or VITE_ prefix.
  3. Decode a JWT that starts with eyJ (legacy keys only): if the payload role is service_role, that key is elevated. Publishable and secret keys are short strings, not JWTs.
  4. 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

BEFOREService role in a client component
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!
)
AFTERPublishable or anon in the browser
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
Never put SERVICE_ROLE or a secret key behind a NEXT_PUBLIC_ or VITE_ name.
BEFOREElevated key imported into shared browser code
// lib/supabase.ts imported by both client and server
export const admin = createClient(url, process.env.SUPABASE_SERVICE_ROLE_KEY!)
AFTERElevated client only in a server file
// app/api/admin/route.ts  (server only)
import { createClient } from '@supabase/supabase-js'
const admin = createClient(url, process.env.SUPABASE_SERVICE_ROLE_KEY!)
Keep the secret key out of any module the bundler can pull into client code.

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. Redeploy and re-search the bundle

    Deploy the fix. Search the new JS again. Zero hits for service_role and sb_secret_.

  6. 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.

  7. 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 SERVICE ROLE IN CLIENT
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

  1. Supabase: API keys
  2. Supabase: Securing your API
  3. Supabase: Row Level Security
  4. Supabase: Creating a server client
  5. 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 repo

Leakcade 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

Patch Notes by Leakcade

A short weekly email: what changed in AI coding tools, one real security mistake, one prompt you can reuse.