PreflightHow it worksPricingSampleGuides

Supabase anon key exposed?

Short answer: that’s expected. The anon or publishable key is built to sit in your frontend. Here is what it can do, and the three things that actually leak data.

Free scan: GET and HEAD requests only, after you verify you own the domain. The two-account cross-user test is the $29 Proof Report. Updated 25 Oct 2026

In short

  • ✓The anon key (legacy) and the publishable key (sb_publishable_…) are public by design. Rotating them does not fix a leak.
  • ✓Anyone with the key gets exactly what your grants and RLS policies allow the anon and authenticated roles. Nothing more.
  • ✓The real problems: tables without RLS, policies that let everyone through, and a service_role or sb_secret_ key in the browser.
  • ✓Supabase is replacing the legacy anon and service_role keys with publishable and secret keys. Plan the switch.

01Is the Supabase anon key safe to expose?

Yes, if your database is set up to expect it. Supabase’s API key docs say the publishable key is safe to expose online, in web pages, mobile apps and source code, and the anon key is its legacy equivalent. Your frontend has to send some key to reach the API, and anything shipped to a browser can be read. That is why the key identifies your project, not a person.

Security scanners, GitHub alerts and well-meaning friends often flag it anyway, because it looks like a secret: a long JWT starting with eyJ. The question to ask is not “is the key visible?” but “what does my database let the anon role do?”

02Anon key vs publishable key vs service_role key

KeyLooks likeRuns asIn the browser?
Publishablesb_publishable_…anon, or authenticated once a user signs inYes
Anon (legacy)JWT, "role":"anon"Same as publishableYes
Secretsb_secret_…Bypasses RLSNever
service_role (legacy)JWT, "role":"service_role"Bypasses RLSNever

To tell the two JWTs apart, decode the middle part. In the browser console:

const k = 'PASTE_THE_KEY_FROM_YOUR_BUNDLE';
JSON.parse(atob(k.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))).role
// "anon" is fine. "service_role" means rotate now.

Supabase is retiring the legacy JWT keys in favour of publishable and secret keys. New keys can be rotated one at a time without downtime, which the legacy pair never allowed.

03What someone can do with your anon key

With the public key alone, a stranger can do whatever your app’s signed-out visitors can do, plus anything your API answers that the UI never shows:

  • Query any table or view the anon role has grants on, filtered by RLS. No RLS means every row.
  • Call database functions through /rest/v1/rpc/ that anon can execute. Postgres grants EXECUTE to everyone by default.
  • Sign up, if sign-up is open, and become authenticated. With email confirmation off, as many accounts as they like.
  • Start an anonymous session if anonymous sign-ins are on. That session is also authenticated.
  • List storage buckets or download from public buckets if your storage policies allow it (storage guide).
  • Call Edge Functions. The legacy anon key is itself a valid JWT, so a function’s built-in JWT check passes with it. Check for a real user inside the function.

So “the key is public” becomes a problem the moment a policy says “signed-in users can read everything”, because anyone can become a signed-in user.

04The three things that actually leak Supabase data

1. Tables with RLS off. Older projects grant anon and authenticated full access to new tables in public, so a table created by SQL or a migration without RLS is open to the whole internet. In September 2026 UpGuard reported 16,326 Supabase databases with at least one anonymously readable table, out of roughly 300,000 domains it probed (UpGuard).

2. Policies that let everyone through. using (true), or a “signed-in users only” policy on data that belongs to one user. See the patterns in the RLS checker guide.

3. A secret key in the frontend. Usually an environment variable with a public prefix (NEXT_PUBLIC_, VITE_, EXPO_PUBLIC_) holding the service_role or secret key. Build tools inline those into the JavaScript every visitor downloads.

05How to check your app in five steps

1. Find the key your site ships. Open your live site, DevTools → Sources, and search for supabase.co, sb_publishable_, sb_secret_ and eyJ.

2. Confirm its role with the decode snippet above. Anything other than anon or publishable: stop and rotate it.

3. List tables without RLS in the SQL editor:

select tablename from pg_tables
where schemaname = 'public' and not rowsecurity;

4. Count what a stranger sees. For each table your app uses:

curl -I "https://YOUR_REF.supabase.co/rest/v1/orders?select=*" \
  -H "apikey: YOUR_PUBLIC_KEY" -H "Prefer: count=exact"
# look at content-range: "*/0" is good, "0-24/1532" means 1,532 rows are public

5. Check what sign-up allows:

curl "https://YOUR_REF.supabase.co/auth/v1/settings" -H "apikey: YOUR_PUBLIC_KEY"
# disable_signup, mailer_autoconfirm and external.anonymous_users tell you
# who can become "authenticated"

06How to lock it down without hiding the key

  • Turn on RLS for every table in exposed schemas and add owner-scoped policies (examples).
  • Revoke grants the anon role never needs: revoke all on table public.orders from anon;
  • Take internal functions off the API: revoke execute on function public.recalc_credits() from public, anon, authenticated;
  • Turn email confirmation on, and anonymous sign-ins off unless you use them.
  • Keep secret keys on the server, in Edge Functions or server routes, never behind a public env prefix.
  • Move to the publishable key and new secret keys, then deactivate the legacy pair once nothing uses it.

Hiding the key behind your own proxy only helps if the proxy enforces rules the database doesn’t. Fixing the rules is the shorter path.

07What Preflight checks about your Supabase key

The free scan reads your deployed JavaScript, finds your Supabase URL and key, and treats an anon or publishable key as the public key it is. It flags a service_role JWT or sb_secret_ key as critical, along with other secret keys such as OpenAI or Stripe. With the public key it counts rows visible signed out on up to 35 tables, reads your auth settings and checks whether your bucket list is public. The Proof Report then tests what a signed-in stranger can reach, as two test accounts you create.

?Questions people ask

  • Should I rotate my Supabase anon key if it was exposed?

    Not because it was seen; it is meant to be seen. Rotate it if you are moving to the new publishable key, or if you rotated the legacy JWT setup for another reason. If data leaked, fix RLS and grants instead.
  • Is it safe to commit the Supabase anon key to GitHub?

    The anon or publishable key, yes, that is how Supabase intends it to be used. Never commit a service_role or sb_secret_ key. If you did, rotate it.
  • What is the difference between the anon key and the service_role key?

    The anon key runs as the anon role and is limited by your grants and RLS. The service_role key runs with a role that bypasses RLS and can read and write every table. Only the anon or publishable key belongs in a browser.
  • Why do security scanners flag my Supabase anon key?

    Pattern-based scanners see a JWT and assume it is a secret. Check its role claim. If it says anon, the finding is a false alarm, and the useful follow-up is checking what the anon role can read.
  • Can someone delete my data with the anon key?

    Only if a grant and an RLS policy allow the anon or authenticated role to delete those rows. Tables with RLS off are the usual way that happens.

What an outside scan can't tell you

Can one user read another user's data?

Preflight signs in as two test accounts you create and tries to read one account's rows and files from the other. That cross-user test is the $29 Proof Report for Supabase apps. Start with the free scan: it checks your live app from outside with GET and HEAD requests only.