Supabase anon key: is it safe to expose, and what can it reach?

The Supabase anon key is the public key your frontend uses to call your project, and it is safe to expose: every browser that loads your app already has it. It gives callers the anon role, and your grants and row level security (RLS) policies decide what that role can read and write. If one table has RLS off, anyone holding the key can read it, and the key is in your JavaScript.

The team behind Keybrum's attacksUpdated 12 min read

In short

  • The anon key is public by design. You cannot hide it and you do not need to: every visitor's browser already has it.
  • It gives the anon role. A table with RLS off that anon can reach is readable by anyone through the REST API, and on most projects created before mid-2026, writable too.
  • Check every table, view, function and bucket the API exposes, not only the ones your app uses.
  • Rotating the anon key protects nothing on its own. Fixing the policies does. Supabase is replacing it with the publishable key by the end of 2026, and RLS works the same with either.
  • The service role key is the opposite: it skips RLS entirely and must never reach a browser.
On this page

What the Supabase anon key is

Every Supabase project comes with a URL and a public key that your frontend uses to talk to the database. In projects that use the legacy keys, that public key is the anon key: a JSON Web Token signed with your project's legacy JWT secret, whose payload says "role": "anon" and names your project reference. The newer publishable key starts with sb_publishable_ and does the same job. Supabase is deprecating the anon key by the end of 2026, so new code and new tutorials use the publishable key. Either way, the key does two things: it tells Supabase which project you mean, and it tells Postgres to treat the caller as the anon role until they sign in.

That is all. The key is not a password and opens nothing by itself. It is closer to a building's street address than to its front door key. Who gets past the door is decided by the database: by the privileges (grants) each role holds, by row level security (RLS) policies, and by the settings of your storage buckets.

KeyLooks likeRole it givesWhere it belongs
Anon key (legacy)eyJhbGciOi... (a JWT)anonBrowser, mobile app, anywhere
Publishable keysb_publishable_...anonBrowser, mobile app, anywhere
Service role key (legacy)eyJhbGciOi... (a JWT)service_role, skips RLSYour server only
Secret keysb_secret_...service_role, skips RLSYour server only
Both legacy JWTs start the same way. Decode the payload, or paste the key in the Supabase key checker, to see which one you have.

Is it safe to expose the Supabase anon key?

Yes, on one condition: every table the API can reach needs RLS enabled, with correct policies. Supabase's documentation lists the publishable key, and the anon key it replaces, as safe to expose in web pages and apps, and tells you to enable RLS on every table before you deploy. The condition is the part that gets skipped.

A table created from the Table Editor gets RLS switched on by default. A table created by a SQL migration, by an ORM, or by an AI coding agent writing create table does not, unless the migration says so. That is how most leaks start: the app works, every screen shows the right data, and one table underneath answers anyone who asks.

There is a second lock, and Supabase changed it in 2026. Grants decide whether the anon role can touch a table at all; RLS decides which rows. For years, every new table in public was granted select, insert, update and delete to anon automatically. New tables now need an explicit grant: the default for new projects from 30 May 2026, scheduled for existing projects on 30 October 2026. Tables that already existed keep their grants. And when a missing grant breaks a page, the error message spells out the grant statement to run, which a coding agent can paste as is on a table where RLS is still off.

What anyone can do with your anon key

Holding the anon key means being able to call your project's APIs as the anon role, from anywhere, without your app. On a table where RLS is off and the grants are in place, that role has the same reach your own code has. Here is what the key opens, if the database lets it:

  • The REST API (PostgREST) at /rest/v1/: select, filter, insert, update and delete rows in every table and view of the exposed schemas that anon holds grants on. The exposed schema is public by default.
  • Database functions at /rest/v1/rpc/: any function in an exposed schema that the anon role may execute. Postgres lets every role execute a new function unless you revoke it.
  • The GraphQL API, when the pg_graphql extension is enabled: the same tables through another door. Supabase stopped enabling it by default on new projects in 2026, but older projects may still have it.
  • Storage: files in public buckets, plus anything private buckets allow through their policies.
  • Auth: creating accounts, if sign-ups are open. A fresh account has the authenticated role, so policies that only check that the caller is signed in let any stranger in after a sign-up. If anonymous sign-ins are enabled, the visitor gets the authenticated role without even an email address.

One detail makes this worse in practice: your table names are not a secret either. Supabase stopped serving the schema listing at /rest/v1/ to the anon key in 2026, but the JavaScript bundle of a typical app contains calls like supabase.from('profiles') and supabase.from('orders'). A visitor who opens the developer tools sees the list of tables your app uses, next to the key.

Where to find your Supabase anon key

  1. 1

    Open your project in the Supabase dashboard

    Click Connect at the top of the project: it shows the project URL and the public key for the framework you pick, ready to paste into .env. To see every key, go to Settings, then API Keys.

  2. 2

    Pick the right tab

    The Publishable and secret API keys tab holds the new keys. The anon key sits on the Legacy API Keys tab. If you see a Create new API keys button, your project still has only the legacy keys.

  3. 3

    Use it in your frontend environment

    Frameworks expect it in a public variable: current Supabase examples for Next.js use NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, older projects often have NEXT_PUBLIC_SUPABASE_ANON_KEY, and Vite apps use a VITE_ prefix instead. The prefix means the value is copied into the browser bundle, which is fine for this key and only this key.

  4. 4

    For local development

    Run supabase start with the Supabase CLI, or supabase status once the stack is running: it prints the local API URL and the local keys. Recent CLI versions print a publishable key (sb_publishable_...) where older ones printed the anon key.

How to check what your anon key can reach

Three checks, in this order. The first two run in the SQL editor of your own project and change nothing. The third reads your own data the way a stranger would.

1. List the tables with RLS off

sql
select n.nspname as schema,
       c.relname as table_name,
       has_table_privilege('anon', c.oid, 'select') as anon_can_read,
       has_table_privilege('anon', c.oid, 'insert,update,delete') as anon_can_write
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.relkind in ('r', 'p')
  and n.nspname = 'public'
  and not c.relrowsecurity
order by 1, 2;
Every row is a table without RLS. Where anon_can_read or anon_can_write is true, the anon key reaches it through the API right now. Add other schemas to the filter if your API exposes them.

Fix each one with alter table public.your_table enable row level security;, then take back the grants anon should not have, for example revoke insert, update, delete on public.your_table from anon;. Policies do not remove grants. With RLS on and no policy at all, the table is closed to anon and authenticated alike, which is the safe default while you write the policies the app needs. The Supabase RLS guide has policies for the common cases: own rows only, team members, public read with owner write.

2. Find policies that let everyone in

sql
select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
  and (qual = 'true' or with_check = 'true')
order by tablename;
A policy whose condition is true lets every caller through. For a select on a table of public blog posts, that can be intended. For profiles, orders or messages, it is a leak.

Also look for policies granted to authenticated that never compare a column to auth.uid(). They pass for any signed-in user, so any stranger who signs up can read every row. That pattern is the most common access control bug we see in generated apps, and it is the database version of an IDOR vulnerability.

3. Read one table as a stranger would

bash
curl "https://YOUR_PROJECT_REF.supabase.co/rest/v1/profiles?select=*&limit=1" \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"
Run it against your own project only. With a publishable key, send only the apikey header: it is not a JWT.

A row in the answer means anyone can read that table. An empty array [] means either RLS filtered everything out or the table is empty, so check a table you know has data. A 42501 error (permission denied) means the anon role has no grant on it, which is also fine, as long as nobody adds the grant before turning RLS on.

Supabase runs similar checks for you. Open Security Advisor in the dashboard, or run supabase db advisors with the CLI: it flags tables in public with RLS disabled, views that bypass RLS, permissive policies, public buckets that allow listing, and security definer functions that anon can execute. Treat each finding as a lead and confirm it with the checks above.

The traps: views, functions, storage and the new keys

Teams that turned RLS on everywhere still leak through four places that are easy to miss.

Views skip the RLS of the tables they read

In Postgres, a view runs with the privileges of its owner by default, and in Supabase that owner is usually postgres, so the policies of the underlying table are skipped. A view over profiles in the public schema can hand the whole table to the anon key while the table itself is locked. On Postgres 15 and later, make the view respect the caller's policies:

sql
alter view public.profile_cards set (security_invoker = true);

Functions marked security definer

A function declared security definer runs as its owner too, and functions in public can be called by the anon role through /rest/v1/rpc/ unless you revoke it. If a function only exists for your server or a trigger, take it away from the API: revoke execute on function public.your_function from anon, authenticated, public;, or move it to a schema the API does not expose.

Storage buckets

A public bucket serves every file to anyone with its URL, with no policy involved; policies still apply to uploads, deletes and moves. Invoices, ID scans and private uploads belong in a private bucket with policies on storage.objects. The Supabase storage guide shows the policy that limits each user to their own folder.

Publishable keys change nothing about RLS

Moving from the anon key to a publishable key is now on a deadline, since Supabase is deprecating the anon key by the end of 2026. It is also easier to manage: publishable keys are not tied to the JWT secret, so you can replace one without touching your other keys. It does not change what a stranger can read: the caller is still anon, and the grants and policies still decide. The anon key vs publishable key guide covers the migration.

When should you rotate the anon key?

Less often than people think. A new public key ships in your next build and is just as visible as the old one. It does nothing for a table that is open. Replace or turn off the anon key when one of these is true:

  • The service role key or the JWT secret leaked. The legacy anon and service_role keys are both JWTs signed by the same legacy JWT secret, so you cannot retire one without the other. Supabase's procedure: fix the cause of the leak, move clients to a publishable key and servers to a new secret key, deactivate the legacy keys in Settings > API Keys, then revoke the legacy JWT secret. Read what to do when the service role key leaks first: that one is an emergency.
  • You finished moving to publishable keys. Deactivating the legacy keys makes old builds that still embed the anon key stop working. With the deprecation, this step is coming anyway.
  • Bots are hammering your API with a copied key. Moving to a publishable key and deactivating the legacy keys cuts off copies of the anon key that never reload your JavaScript, which buys time while you add rate limits. Anything that reads your current bundle gets the new key.

Plan it before you click. Deactivating the legacy keys breaks every deployed client that still sends them, including mobile app versions already in users' hands, until they ship with a new key. Rotating the legacy JWT secret the old way also signs out every active user; Supabase's JWT signing keys system avoids that, which is one more reason to migrate.

Anon key or service role key: the one rule

The anon key is safe in a browser because the database checks every request against your grants and policies. The service role key is not, because it tells the database to skip the policies. If a frontend file, a mobile app or a public repository contains a key whose payload says service_role, or a key starting with sb_secret_, treat it as a breach: replace it, retire the leaked key, then find out what was read. Our guide to API keys exposed in the frontend lists the other keys that belong only on a server.

The SQL above tells you what your policies say. It cannot tell you what a stranger can actually do with them across your whole app, signed out, then signed in as someone else. That is what an attack shows.

Questions

Is the Supabase anon key a secret?

No. It is meant to be public and ships in your frontend code. It identifies your project and gives callers the anon role. Your data is protected by grants and row level security policies, not by keeping the key hidden.

Where do I find my Supabase URL and anon key?

In the dashboard, click Connect at the top of your project: it shows the project URL and the public key for your framework. Every key is listed under Settings > API Keys, with the anon key on the Legacy API Keys tab. Newer projects use a publishable key (sb_publishable_...), which does the same job.

Can I hide the anon key in an environment variable?

Not from the browser. Variables starting with NEXT_PUBLIC_ or VITE_ are copied into the JavaScript bundle at build time, so every visitor can read them. That is expected for the anon key. It is a serious problem for any other key, see API keys exposed in the frontend.

What happens if I enable RLS on a table without any policy?

Nobody using the anon key or a user session can read or write it: RLS denies by default. Only the service role and the database owner still get through. Your app will show empty lists until you add policies for what it needs.

Can someone delete my data with the anon key?

Only if the database lets them. On projects created before Supabase changed its defaults in 2026, tables in public came with write grants for anon, so a table with RLS off can be changed or emptied by anyone. With RLS on, writes need a policy for insert, update or delete.

Is the publishable key safer than the anon key?

It gives the same anon role, so it reaches exactly the same data. It is easier to manage, because it is not tied to the JWT secret, and Supabase is deprecating the anon key by the end of 2026. See the comparison.

How do I know if my Lovable or Bolt app has this problem?

Run the two SQL queries in this guide in your Supabase project, and open Security Advisor in the dashboard. For an outside view, paste your app's address into the free check: it shows the key and the tables your JavaScript names in about ten seconds, without sending anything to your database.

Sources

  1. 1.API keys, Supabase Docs
  2. 2.Row Level Security, Supabase Docs
  3. 3.Securing your API, Supabase Docs
  4. 4.Breaking Change: Tables not exposed to Data and GraphQL API automatically, Supabase Changelog
  5. 5.Rotate a leaked or compromised API key, Supabase Docs
  6. 6.JWT Signing Keys, Supabase Docs
  7. 7.Advisors, Supabase Docs
  8. 8.Storage buckets, Supabase Docs
  9. 9.CREATE VIEW (security_invoker), PostgreSQL documentation
  10. 10.Statement on CVE-2025-48757, Matt Palmer
  11. 11.CVE-2025-48757, NIST National Vulnerability Database

Your app is public. So are its mistakes.

Check what any attacker can already see in it, free, in ten seconds. The full attack, with proof and fixes: €49 once, or €29 a month on a plan.

or continue with Google or GitHub