What the Supabase key checker tells you
Supabase gives every project two kinds of keys: a public one that your frontend ships to every visitor, and a server one that skips every security rule. They are easy to mix up. The legacy keys are both long JWTs starting with eyJ, and the only thing that tells them apart is one word hidden in the middle: the role. This checker decodes that part in your browser and names the key, so you know where it may live before you paste it anywhere else.
| Key | Looks like | Role | In a browser | If it leaked |
|---|---|---|---|---|
| Publishable key | sb_publishable_... | anon | Yes | Nothing to rotate: check RLS |
| Legacy anon key | eyJ... with "role": "anon" | anon | Yes | Nothing to rotate: check RLS |
| Secret key | sb_secret_... | service_role | No, server only | Replace it, then delete it |
| Legacy service_role key | eyJ... with "role": "service_role" | service_role | No, server only | Move to new keys, switch legacy off |
| Personal access token | sbp_... | You, on the Management API | Never in an app | Revoke it |
| User session token | eyJ... with sub and email | authenticated, one user | Not an API key | It expires on its own |
Nothing you paste leaves the page: there is no request to Supabase or to Keybrum, and the input is never logged. That also means the checker reads the key without proving it works, which matters for the limits below.
Each check, and why it matters
- Role. A key with the
anonrole gets only what your row level security (RLS) policies allow. A key with theservice_rolerole has Postgres'sBYPASSRLSattribute: it reads, changes and deletes every row, every stored file and every user account, whatever your policies say. - Project. Legacy keys carry the project reference (
ref). Add your project URL and the checker compares the two: a key from another project is answered with a 401, which is a common cause of an app that suddenly shows nothing. Publishable and secret keys do not carry the ref, so they cannot be matched this way. - Issued and expiry. The
iatandexpclaims are shown as dates. An expired key is refused by Supabase. A user's session token lives for a short time and is refreshed by the client. - Where it sits. When you paste a file, the checker reads the variable name. Values in
NEXT_PUBLIC_,VITE_,EXPO_PUBLIC_orREACT_APP_variables are copied into the JavaScript bundle at build time, so a server key there is already public. It also catches the classic slip: a variable namedANON_KEYthat holds the service role key. - Demo keys. A payload with
iss: supabase-demois the example key of self-hosted Supabase. Its JWT secret is published on GitHub, so an instance that still trusts it lets anyone sign a service_role key.
How to read the result
The first line is the verdict, colored by what it means for you. Green: a public key, fine in a browser. Red: a key that must stay on a server, with the steps to retire it if it ever left. Amber: something to look at, such as a session token, an expired key or a key from the wrong project. Each row below the verdict says what was found, why it matters and the fix, with a copy button.
A green verdict is about the key, not about your data. The anon and publishable keys are public by design: what they can read is decided table by table by your policies. That is where real leaks happen. CVE-2025-48757 was assigned after 170 of 1,645 Lovable apps tested let strangers read or write data through missing or weak RLS, with a public key that was working exactly as intended. Our guide to the Supabase anon key shows what that key reaches, and the Supabase RLS checker lists the tables it can read.
How to fix a service role key that went public
If a service_role or secret key was ever in a frontend, a git repository, a screenshot or a chat, treat it as leaked. Deleting it from the code does not take it back: old builds and git history keep it. Fix the cause first, then retire the key on Supabase's side. For a legacy service_role key, Supabase's current flow is:
- 1
Move the key out of the frontend
Read it only in server code: an API route, a server action or an Edge Function. Remove any public prefix from the variable name.
- 2
Create the new keys
In Settings > API Keys, create a secret key for your server and a publishable key for your apps. Deploy both.
- 3
Switch the legacy keys off
In Settings > API Keys, deactivate the legacy keys. This retires the anon and the service_role key together, so do it after every client uses the new keys.
- 4
Revoke the legacy JWT secret
In Settings > JWT Keys, migrate the legacy secret to signing keys, rotate, then revoke it. Supabase notes that anything still trusting that secret can accept the leaked key.
- 5
Look for damage
Check your tables, storage buckets and Authentication > Users for changes you did not make.
A leaked sb_secret_ key is simpler: create a new secret key, swap it everywhere, then delete the old one in Settings > API Keys. Deleting cannot be undone, and the publishable key and user sessions are not affected. The server side should look like this:
// app/api/admin/route.ts: server only, never imported by a component
import { createClient } from "@supabase/supabase-js";
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SECRET_KEY!, // no NEXT_PUBLIC_ or VITE_ prefix
{ auth: { persistSession: false, autoRefreshToken: false } },
);Moving from the anon key to a publishable key is the same idea on the browser side, and changes nothing about what the key can reach. See anon key vs publishable key for the migration.
What a static check cannot tell you
- Whether the key still works. The checker never calls Supabase, so it cannot tell an active key from a deactivated one. It also does not verify the signature: an edited token decodes the same way.
- Where the key has been. It reads the key you paste. It cannot know whether that key is in your live bundle, an old deploy or a public repository.
- What your policies allow. A public key is only as safe as your RLS. The checker does not see your tables.
- Which project owns a new key. Publishable, secret and access tokens carry no project ref.
For the keys that are not from Supabase (OpenAI, Stripe, AWS), use the API key leak checker. If a key already leaked, what to do with a leaked API key gives the order to fix things in.
Questions
Is the Supabase anon key safe to expose?
Yes, if row level security is on, with sound policies, for every table your API exposes. The anon key and the publishable key are meant to ship in your frontend. They give the anon role, and your policies decide what that role reads. See the anon key guide.
How do I tell the Supabase anon key from the service role key?
Both legacy keys are JWTs starting with eyJ. Decode the middle part: it says "role": "anon" or "role": "service_role". The checker above does it in your browser. With the new keys it is visible at a glance: sb_publishable_ is public, sb_secret_ is server only.
What is the difference between sb_publishable and sb_secret keys?
The publishable key replaces the anon key and is safe in a browser. The secret key replaces the service_role key, skips RLS and belongs on a server. Supabase also refuses a secret key sent from a browser, but that check reads the User-Agent header, which a script can set to anything.
What should I do if my Supabase service role key was exposed?
Move it to server-only code, create new secret and publishable keys, deactivate the legacy keys, then revoke the legacy JWT secret in Settings > JWT Keys. Then check your data for changes. The service role key guide walks through each step.
Is it safe to paste my Supabase key into this checker?
The checker runs entirely in your browser: no request is made, nothing is stored or logged. Still, a service role key you pasted anywhere is a key you handled outside your server. If it came from a frontend or a repo, rotate it either way.
Where do I find my Supabase keys?
In the Supabase dashboard, under Settings > API Keys, or in the Connect dialog of your project, which shows the URL and the publishable key for your framework. Personal access tokens are under Account > Access tokens.