Supabase keys: which ones are safe to expose
The anon key and the newer sb_publishable_ key are designed to sit in your front end, and they are only safe when row level security is on for every table. The service_role key and sb_secret_ keys bypass RLS completely, so they must stay on a server.
Updated
Check your live site now
Free, no signup, read only. A grade and plain fixes in seconds.
The four Supabase keys and what they look like
Supabase has two generations of API keys, and plenty of projects have both. Two of them are public by design and two are admin keys. The names in your code don't matter; the key itself does.
- Publishable key, starting sb_publishable_. The current public key. It goes in your front end and identifies your project.
- anon key, a long JWT starting eyJ. The legacy public key. Its payload contains a role claim set to anon.
- Secret key, starting sb_secret_. The current server key. Full access, skips RLS. You can create several and delete them one at a time.
- service_role key, also a JWT starting eyJ, with the role claim set to service_role. The legacy server key, with the same power as a secret key.
Your project URL isn't a secret either. The public keys put a request into the anon role, or the authenticated role once a user signs in. What those roles can read and write is decided entirely by your RLS policies, not by the key.
Legacy anon and service_role keys look almost identical. To tell them apart, decode the middle part of the JWT on your own machine (don't paste an admin key into a website) and read the role claim.
# Prints the payload of a legacy Supabase key. Look for "role".
echo "$SUPABASE_KEY" | cut -d. -f2 | base64 -d 2>/dev/null; echoWhy the anon key is only as safe as your RLS
Everyone who loads your site has your public key, and that is expected. Your RLS policies protect the data, not the key. If a table in an exposed schema has RLS switched off, anyone with that key can read it, and often write to it, straight through the REST API without ever touching your app.
The Security Advisor in the Supabase dashboard flags tables without RLS. To see a table the way a stranger would, call it with only the public key:
curl "https://YOUR_PROJECT.supabase.co/rest/v1/profiles?select=*&limit=5" \
-H "apikey: YOUR_PUBLISHABLE_OR_ANON_KEY"For a private table you want an empty array or a permission error. Real rows coming back means the whole internet can read them. A passive scan like ours can't run this check for you, because it never queries your database; it only sees what your site sends to a browser.
What someone can do with a leaked service_role or secret key
RLS does not apply to these keys at all. Whoever holds one can:
- Read every row in every table the API exposes, including other users' private data.
- Insert, update and delete anything, including wiping tables.
- Read and overwrite files in Storage regardless of your storage policies.
- Use the auth admin API to list, create, change or delete your users.
Supabase refuses sb_secret_ keys on requests that look like they come from a browser, which catches honest mistakes. It doesn't stop someone who copies the key into a script, so a secret key in a bundle is still a full leak.
What to do if your service_role or secret key leaked
- Replace the key before anything else. For an sb_secret_ key, open Project Settings, then API Keys, create a new secret key, point your servers and Edge Functions at it, and delete the leaked one.
- For a legacy service_role key, there is no way to swap it alone, because it's signed with the same legacy JWT secret as the anon key. Move to the new publishable and secret keys, ship the publishable key to your front end, then disable the legacy keys in the same settings area.
- Remove the key from your code, from any front-end env var (NEXT_PUBLIC_, VITE_, EXPO_PUBLIC_) and from committed .env files, then redeploy.
- If it was ever committed, scrub it from git history. That doesn't un-leak it, which is why the new key comes first.
- Check what happened. Read the API and auth logs in your dashboard for traffic you don't recognise and look for unexpected changes in sensitive tables. If personal data was readable, you may have obligations to tell your users, depending on where you operate.
# 1. Put the leaked value in a file, one per line, mapped to a placeholder
echo 'PASTE_THE_LEAKED_KEY_HERE==>REMOVED' > replacements.txt
# 2. Rewrite every commit (work on a fresh clone, keep a backup)
git filter-repo --replace-text replacements.txt
# 3. Force push the rewritten branches, then delete replacements.txt
git push --force --allHow to use the service_role key safely on a server
Anything that needs to skip RLS belongs in code the user never downloads: an Edge Function, a server route, a scheduled job. Edge Functions get the service role key as a built-in environment variable, so you never paste it anywhere.
import { createClient } from "npm:@supabase/supabase-js@2";
Deno.serve(async (req) => {
// 1. Work out who is calling, using their own session token.
const userClient = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{ global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } } },
);
const { data: { user } } = await userClient.auth.getUser();
if (!user) return new Response("Unauthorized", { status: 401 });
// 2. Only now use the admin client, for this one job.
const admin = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);
const { error } = await admin.from("audit_log").insert({ user_id: user.id, action: "export" });
return new Response(error ? "Failed" : "OK", { status: error ? 500 : 200 });
});- Never give the admin key a public prefix, and never pass it from a server component to a client component as a prop.
- Check who the caller is before doing anything with admin rights. An Edge Function that runs admin queries for any caller gives strangers the same power as the key itself.
- Prefer the user's own session plus good RLS policies. Reach for the admin key only for jobs that truly need to cross users.
Questions
Is the Supabase anon key safe to expose?
Yes, it's designed to be public. It's only as safe as your row level security, though: any table without RLS is readable, and often writable, by anyone who has the key.
What is the difference between the anon key and the publishable key?
They do the same job. The publishable key (sb_publishable_) is the newer format. It isn't a JWT and doesn't depend on the legacy JWT secret, so you can create and delete publishable keys independently.
Can I use the service_role key in my Lovable or Bolt front end if it's in an env var?
No. Env vars that reach front-end code are copied into the JavaScript bundle at build time, so anyone can read them. Put the admin work in an Edge Function and call that instead.
Does your scan detect a leaked service_role key?
Yes. It looks for sb_secret_ keys and for JWTs whose role claim is service_role in your page HTML and your site's own JavaScript. Anon and publishable keys are correctly treated as public and not flagged.