Abbi Labs

Guides

Is your Supabase anon key safe to expose? Yes, until your RLS says otherwise

By Pritesh Ghodge, security engineer · Last updated 4 November 2026

Find out which Supabase key your live app ships, and what it can read.

The Abbi Labs quick check reads your site's public JavaScript, flags service_role or secret keys, and tries to read one row from your Supabase tables with the public key. It also checks exposed .env/.git files and security headers. No sign-up.

No sign-up. Read-only GET requests to what your site already serves publicly. Scan only sites you own or are allowed to test.

Weekly AI-app security checklist

One short email a week: a practical check for apps built with Supabase, Next.js and AI tools (RLS, leaked keys, headers and more). Unsubscribe anytime.

Short answer: yes, the anon key is meant to be public. It's in your JavaScript bundle on purpose, and anyone who opens DevTools can copy it. Supabase's own API keys docs describe the publishable key (the newer name for the anon key) as safe to expose in web pages, mobile apps and source code.

The catch is in what "safe" means. The key isn't a lock. It's a label that says "this request comes from your public app", and Postgres maps it to the anon role (or authenticated, once a user signs in). Everything that role is allowed to do, a stranger with your key can do too. So the real question is never "is my key exposed?" It's "what does my database let the anon and authenticated roles do?"

The four Supabase keys

KeyLooks likePostgres roleIn the browser?
Publishablesb_publishable_…anon / authenticatedYes, by design
anon (legacy)long JWT starting eyJanon / authenticatedYes, by design
Secretsb_secret_…service_role (bypasses RLS)Never
service_role (legacy)long JWT starting eyJservice_role (bypasses RLS)Never

Supabase says it is deprecating the legacy anon and service_role keys by the end of 2026 in favour of publishable and secret keys. Creating the new keys doesn't switch off the old ones; you disable those separately in Settings → API Keys.

Check 1: which key is actually in your live app?

This is where AI-built apps go wrong. The anon key and the service_role key look almost identical (both start with eyJ), and an AI assistant fixing a "permission denied" error will sometimes swap in the one that makes the error go away.

Open your live site, then DevTools → Network, and click any request to supabase.co. Copy the apikey header.

const k = "KEY";
JSON.parse(atob(k.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))).role

"anon" is the public key. "service_role" means anyone visiting your site can read, change and delete every row, whatever your RLS says. Follow the service_role key exposed guide straight away.

Also search your built JavaScript (DevTools → Sources, or Ctrl/Cmd+Shift+F) for service_role and sb_secret_. A secret key can sit in a bundle without being used on the page you loaded. The NEXT_PUBLIC_ and VITE_ guide explains how server keys end up there.

Check 2: what can a logged-out stranger read?

Only test your own project. With your project URL and public key, ask for one row of a table:

curl -s "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*&limit=1" \
  -H "apikey: YOUR_PUBLIC_KEY"

Supabase says to send publishable keys in the apikey header only. Older guides also add Authorization: Bearer with the legacy eyJ anon key, which works too. An empty [] or a permission error means a logged-out visitor can't read that table. A real row means anyone can download the whole table with the key from your site. Repeat for every table your app uses. The Supabase RLS check guide walks through this test and the fix SQL in detail.

Check 3: what can they write, and what does "signed in" mean?

Reads are only half of it. Your public key can also insert, update and delete wherever your policies allow it. And if email sign-ups are open, anyone can create an account in your app with that same key, which puts them in the authenticated role. So a policy like to authenticated using (true), or one that only checks auth.uid() is not null, really means "any stranger who signs up".

List your policies:

select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename, cmd;

Look for true in qual or with_check, UPDATE policies without a with_check, and policies on user-owned tables that never mention auth.uid(). Or paste the output of one read-only query into the free Supabase RLS policy checker, which flags all of these in your browser.

Check 4: which database functions can anon call?

Postgres functions in the public schema can be called through /rest/v1/rpc/<name> by any role with EXECUTE on them, and functions are executable by everyone unless that's revoked. A security definer function runs with its owner's rights and skips RLS. List the ones the public key can call:

select p.proname, p.prosecdef as security_definer
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
  and has_function_privilege('anon', p.oid, 'execute')
order by p.prosecdef desc, p.proname;

For each one, ask whether a logged-out stranger should be able to run it. If not: revoke execute on function public.fn_name(arg types) from anon, public; (and from authenticated if signed-in users shouldn't call it either). The security definer guide covers views and functions that bypass RLS.

Things that don't help

Abuse the key does allow

Even with perfect RLS, your public key lets anyone make the requests your app makes. Worth a look before launch:

A prompt for Lovable, Bolt or Cursor

Audit how this app uses Supabase keys:
1. Confirm the frontend only uses the publishable/anon key. Search the
   whole codebase for service_role, sb_secret_ and SUPABASE_SERVICE_ROLE_KEY
   and move any use of them into server code or an edge function.
2. List every table in public and its RLS policies. Every user-owned
   table needs RLS on and policies that compare auth.uid() to the owner
   column, with WITH CHECK on insert and update. Remove using (true) and
   with check (true) on tables that hold user data.
3. List functions in public that anon can execute and revoke EXECUTE on
   any that a logged-out visitor shouldn't call.
4. Show me the SQL before you apply anything. Don't fix errors by
   disabling RLS or switching to the service_role key.

What an outside scan can and can't tell you

From outside, a scanner can see which key your live site ships and what the public key can read without logging in. It can't see your policies, test writes safely, or tell whether one signed-in user can read another's rows. For those, run checks 3 and 4 above and test with two accounts: sign in as user B and try to read or change user A's rows by id.

Keep it that way

Keys get swapped and policies get loosened in the middle of a late-night "fix this error" session. Abbi Labs scans your live app for leaked keys, Supabase tables readable with your public key, exposed .env/.git files and security headers. Monitor re-scans weekly and after every deploy, and emails you only when something new appears.

Run a free scan

FAQ

Is it safe to put the Supabase anon key in my frontend code?

Yes. Supabase designed the anon key (and its replacement, the sb_publishable_ key) to be public: it ships in your JavaScript and anyone can copy it from DevTools. It only reaches what your table grants and Row Level Security policies allow for the anon and authenticated roles. The keys that must never reach the browser are service_role and sb_secret_ keys.

My key starts with eyJ. Is it the anon key or the service_role key?

Both legacy keys are JWTs that start with eyJ, so the prefix doesn't tell you. Decode the middle part of the key and read the role claim: "anon" is the public key, "service_role" is the secret one that bypasses RLS. The browser console snippet on this page does it locally.

Should I rotate my anon key because it's visible?

No. Rotating doesn't help, because the new key will be just as visible in your site's JavaScript. If a scanner or GitHub alert flagged it, confirm it really is the anon/publishable key, then spend the time on RLS and grants. If it turns out to be a service_role or sb_secret_ key, replace it now.

Can someone use my anon key to run up my Supabase bill or spam my app?

They can make the same requests your app makes: read and write whatever your policies allow, sign up if sign-ups are open, call database functions anon can execute, and call your edge functions. Rate limits, CAPTCHA on sign-up and auth checks inside your edge functions are what limit abuse, not hiding the key.

Does Abbi Labs check this?

The no-sign-up quick check reads your live site's JavaScript, flags a service_role or sb_secret_ key as critical, and, if it finds your Supabase URL and public key, tries to read one row from tables your app references plus common table names, storing only a yes or no. It reads only; it doesn't test writes or call your database functions. The free RLS policy checker covers write policies from one read-only SQL query.

Related guides

Weekly AI-app security checklist

One short email a week: a practical check for apps built with Supabase, Next.js and AI tools (RLS, leaked keys, headers and more). Unsubscribe anytime.