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.
# .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.
# 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:
- Rotate the key now in your Supabase project's API settings. Until you do, the old key works for anyone who copied it.
- Move the privileged call to a server, usually a Supabase Edge Function, where the key lives in a server-side secret.
- Remove the variable from the frontend env file, and add the file to
.gitignore.
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
[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
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.
Related findings
AI, payment or database key in the code
A secret key for an AI, payment or database service is sitting in your current code, readable by anyone who can read the repo.
PL-011Key still in git history
You deleted the key. Every old commit still has it, and so does every clone and fork.
PL-003RLS enabled but no policies (owner locked out)
Not a leak, yet. The table is locked to everyone, and the quick fix people reach for is the leak.
Guides
- What to do in the first hour after a leak
- How to rotate a leaked API key and clean your git history
- How to secure a Lovable app’s Supabase database
- How to secure Supabase Edge Functions
- Pre-launch security checklist for vibe-coded apps
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.
Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.