PL-010criticalCode

Service role key in client code

The key that ignores every access rule, shipped to every visitor's browser.

Severity
Critical
Who can exploit it
Anyone who opens your site
Found by
Key scan of the code (every check)
Lab proof
Re-scan; the real fix is rotation

What it is

Supabase gives every project two kinds of key. The public key (anon, or sb_publishable_… on newer projects) is meant to be in the browser and is limited by RLS. The service role key (service_role, or sb_secret_…) bypasses RLS entirely. It is meant only for servers.

If the service role key is in frontend code, every policy in your database stops mattering: anyone can copy it from your JavaScript and read or delete everything. A common route in is an environment variable with a VITE_ or NEXT_PUBLIC_ prefix, which the build tool deliberately copies into the browser bundle.

Example of vulnerable code

Simplified from the patterns we see in AI-built apps. Synthetic: not taken from any real app.

src/lib/admin.ts and .env (vulnerable)
# .env
VITE_SUPABASE_URL=https://xyz.supabase.co
VITE_SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIs…   # shipped to every browser

// src/lib/admin.ts
export const admin = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY,
);

Check it yourself

Search the code and the built bundle. Older Supabase keys are JWTs; decode the middle part locally to see which role a key carries.

check-pl-010.sh
# 1. In the repo: names and prefixes that mean "server-only key"
grep -rnE "service_role|SERVICE_ROLE|sb_secret_" --exclude-dir=node_modules .

# 2. In the built site (after npm run build)
grep -l "service_role" dist/assets/*.js

# 3. Which role does a JWT-style key carry? (runs locally, sends nothing)
node -e "console.log(JSON.parse(Buffer.from(process.argv[1].split('.')[1],'base64url')))" "<the key>"
# "role":"service_role" means it bypasses RLS

The fix

No migration fixes a leaked key. The fix is in three parts, and the report gives each one for your app:

  1. Rotate the key now in your Supabase project's API settings. Until you do, the old key works for anyone who copied it.
  2. Move the privileged call to a server, usually a Supabase Edge Function, where the key lives in a server-side secret.
  3. Remove the variable from the frontend env file, and add the file to .gitignore.
the code change the report suggests
const { data } = await admin.from('profiles').select('*');
const { data, error } = await supabase.functions.invoke('list-profiles');

// supabase/functions/list-profiles/index.ts (runs on the server)
const admin = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
);
// check the caller is an admin here before returning anything

Code findings come with the file, the line and a one-line fix in the report, for your engineer to make. PatchLabs never pushes to your repo.

How PatchLabs finds it

key scan · report excerpt
[SCAN]    src/lib/admin.ts
          service role  eyJh…Q1x8  LEAK
[ACTION]  rotate in Supabase API settings
          move call to an Edge Function
[RESCAN]  current code → 0 keys     OK
          still in history → PL-011
synthetic exampleMasked, not a real key

Keys can't be lab-proven the way access rules can. The only proof that a leaked key is harmless is that it no longer works, and only you can rotate it. What we confirm on the re-check is that no server-only key remains in the current code. The old key stays in git history, which is why rotation, not deletion, is the fix (see PL-011).

When the pattern is intended

The service role key is correct in server code: Edge Functions, a backend, scheduled jobs and CI, loaded from a secret store, never with a public-prefix variable name. A key read from a secret store at run time (Deno.env in an Edge Function) never appears in your code, so there is nothing to flag. For every key the scan does find, the report shows the file or commit, so you can tell a server script from a browser bundle at a glance. The public key belongs in frontend code.

Guides

Free check

Does your app have PL-010?

Paste your public repo. PatchLabs Code reads it for this and every other code finding, and the Lab rebuilds your database to test who can read what.

Free for your first appReads your public repo onlyOr run the free static RLS check

Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.