How to check3 min readUpdated 2026-08-03

Supabase service_role key exposed: how to check and fix

There are two Supabase keys. One is meant to be public. The other bypasses all your security — and they look almost identical.


Supabase issues two keys that look alike — both are long JWTs starting eyJ. One is designed to sit in your frontend. The other must never leave your server. Confusing them is one of the most damaging mistakes in this stack.

Telling them apart

Both are JSON Web Tokens, so you can read the role out of the middle segment. Decode it and look at the role claim: anon is the public one, service_role is the dangerous one.

Decode a key you found

# paste the key in place of THE_KEY
echo "THE_KEY" | cut -d. -f2 | base64 -d 2>/dev/null | grep -o '"role":"[^"]*"'

Check whether yours is exposed

One minute

  1. 1Open your live app and press F12.
  2. 2Sources tab → search all files (Ctrl+Shift+F) for: service_role
  3. 3Also search for: SUPABASE_SERVICE — a mis-prefixed env var is the usual cause.
  4. 4Any hit means the key is in the bundle every visitor downloads.

The most common route in: naming it with a client-exposed prefix. In Vite that is VITE_, in Next.js NEXT_PUBLIC_, in Create React App REACT_APP_. Anything with those prefixes is compiled into the browser bundle by design.

If it is exposed

  1. 01Rotate it now: Supabase dashboard → Settings → API → reset the service_role key. Do this before touching the code — the old key keeps working until you do.
  2. 02Update wherever it was legitimately used: server functions, backend environment variables. Redeploy and confirm those still work.
  3. 03Then fix the code path that leaked it, using the prompt below.
  4. 04Check for use you did not cause: project → Logs → API, filtered to the period since exposure. Look for bulk SELECT queries against user tables from an IP that is not your server.
  5. 05Check Auth → Users for accounts you did not create, especially any with elevated roles, and your tables for rows or deletions you cannot account for.

Where the key should have been instead

Most work that reaches for service_role does not need the key in the app at all — it needs one operation to run with elevated rights. Move that operation, not the key: a Supabase Edge Function holds the key server-side, does the privileged step, and returns only the result. The browser calls a function; it never sees a credential.

The shape of the fix

// supabase/functions/admin-summary/index.ts — runs on Supabase, not in a browser
const admin = createClient(
  Deno.env.get("SUPABASE_URL")!,
  Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, // never prefixed VITE_ / NEXT_PUBLIC_
);

Deno.serve(async (req) => {
  // check who is calling before doing anything privileged
  const jwt = req.headers.get("Authorization")?.replace("Bearer ", "");
  const { data: { user } } = await admin.auth.getUser(jwt ?? "");
  if (!user) return new Response("Unauthorized", { status: 401 });

  const { count } = await admin.from("orders").select("*", { count: "exact", head: true });
  return Response.json({ count });
});

Note the authentication check. An Edge Function holding the service_role key with no check on who is calling it is the same exposure with extra steps — the key is hidden but its power is not.

The usual route in is naming it with a client-exposed prefix — VITE_SUPABASE_SERVICE_ROLE_KEY or NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY — which compiles it straight into the browser bundle. The second most common cause is an AI tool reaching for the service_role key to make a query "just work" when RLS was blocking it.

Paste this into your AI coding tool
Find and remove every use of the Supabase service_role key from client code.

1. Search the project for: service_role, SUPABASE_SERVICE_ROLE_KEY, and any
   client-exposed variable (VITE_*, NEXT_PUBLIC_*, REACT_APP_*) that holds
   a Supabase key. Decode any long eyJ token you find and report its role
   claim.
2. Anywhere the service_role key is used in code that runs in the browser,
   replace it with the anon key and make the operation work through RLS
   instead.
3. If an operation genuinely requires bypassing RLS (admin tooling, batch
   jobs), move it into a Supabase Edge Function or server route that reads
   the key from a server-only environment variable.
4. Confirm no remaining client bundle contains a token whose role claim is
   service_role.

List every place the key appeared. I will rotate it in the dashboard —
tell me explicitly that I must, because the old key stays valid until I do.

Common questions

What is the difference between the anon and service_role keys?
Both are long JWTs starting eyJ and they look almost identical. The anon key is designed to sit in your frontend and is constrained by Row Level Security. The service_role key bypasses RLS completely — every policy you wrote is skipped, and whoever holds it can read, modify and delete every row in every table, for every user.
How do I tell which key I have?
Both are JSON Web Tokens, so the role is readable in the middle segment. Decode it and look at the role claim: anon is the public one, service_role is the dangerous one.
How does the service_role key end up in the browser?
Almost always by being named with a client-exposed prefix — VITE_, NEXT_PUBLIC_ or REACT_APP_ — which compiles it into the bundle by design. The second most common cause is an AI tool reaching for the service_role key to make a query 'just work' when RLS was blocking it.
I rotated the key. Am I done?
Rotating is necessary but not sufficient. If the key was public, assume the data was read — RLS protects you from here on, it does not un-copy anything already taken. Check your API logs for bulk SELECTs you did not cause, and Auth → Users for accounts you did not create. Depending on where you operate and what those tables held, this may be a reportable data breach.

This is one check out of 40+

Paste your site address and we run the whole list from the outside — leaked keys, open databases, unprotected pages — then hand you one prompt that fixes what we find. Free, about 30 seconds, no signup.

Check my site — free