LatchDog

← Guides

Is the Supabase anon key safe to expose? Publishable vs secret keys

Short answer: yes, the Supabase anon key, and its replacement the sb_publishable_ key, is meant to be shipped in your frontend, but only when Row Level Security (RLS) is on for every table and view it can reach, because RLS is what stands between that key and your data. The matching secret keys, service_role and sb_secret_, bypass RLS and must never reach a browser, a mobile app or your repository. If one did, create a new secret key, deploy it, confirm nothing still uses the old one, and only then retire the old key.

The four Supabase keys

KeyWhat it looks likePostgres roleIn the browser?
Publishablesb_publishable_...anon when signed out, authenticated when signed inYes, with RLS
Secretsb_secret_...service_roleNever
anon (legacy)a long JWT starting eyJ, with "role":"anon"anonYes, with RLS
service_role (legacy)a long JWT starting eyJ, with "role":"service_role"service_roleNever

Supabase describes the publishable key as low privilege and "safe to expose online", and the secret key as elevated, "only use in backend components" (Supabase: API keys). The new keys are short strings, not JWTs. Supabase says it is deprecating anon and service_role "by the end of 2026", and that both sets of keys work at the same time, so creating new keys does not switch the old ones off (Supabase: JWT signing keys). The publishable key carries the same low privileges as the anon key, so your RLS policies behave the same after you swap it in (Supabase: migrating to new API keys).

What a publishable or anon key can and can't do

The key only picks a Postgres role. Postgres checks table grants first and then applies RLS, so what the key can do depends on both:

State of the tableWhat anyone holding the public key can do
RLS off, anon has grantsRead and write it. Supabase: "A table in an exposed schema without RLS is readable and writable by any role with a grant on it" (Supabase: RLS). On existing projects, new tables in public have been created with every privilege granted to anon, authenticated and service_role (Supabase: securing your API).
RLS on, no policies yetNothing. Supabase: once RLS is on, no data is accessible with a publishable key until you create policies.
RLS on, a policy to anon using ( true )Read every row. Correct for data that is meant to be public, a leak for anything else.
RLS on, owner-only policies to authenticatedSigned out: nothing. Signed in: only that user's own rows.
A view without security_invoker = trueWhatever the view's owner can read, because views bypass RLS by default. See the view-bypass guide.

Two things the key does not do. It doesn't hide itself: Supabase lists reverse engineering, the browser's network inspector, XSS, CSRF, phishing and man-in-the-middle attacks as things publishable keys "aren't intended to protect against", because anyone can retrieve it from a public component. And "publishable" doesn't mean "anonymous": the same key is used by signed-in users, who then run as authenticated, so your policies need to cover both roles.

One date to know: on October 30, 2026 Supabase applies its "new tables are not exposed to the Data API by default" change to all existing projects. New tables in public then need an explicit grant, but existing tables "keep their current grants and stay reachable" (Supabase changelog). A table you created last year with RLS off is not made safe by that change. Also, Supabase scheduled April 8, 2026 as the date all existing projects stop serving the API's schema listing to the anon key (Supabase changelog), but table names are still easy to find in your own JavaScript.

Why service_role and sb_secret_ must never ship

How to check your own app

1. See which keys your site ships. Open the live site, view the loaded scripts in DevTools (Sources, or Network filtered to JS) and search for sb_secret_, service_role and sb_publishable_. For a legacy key starting eyJ, read the middle part to see its role claim, locally rather than in an online decoder:

echo 'PASTE_THE_KEY' | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null; echo

"role":"anon" is the public key. "role":"service_role" is a leak: go to the rotation steps below.

2. Test what the public key can read. Only test your own project, and use the publishable or anon key your site already ships, never a secret key. The new keys go in the apikey header:

curl "https://YOUR-PROJECT-REF.supabase.co/rest/v1/YOUR_TABLE?select=*&limit=1" -H "apikey: YOUR_PUBLISHABLE_OR_ANON_KEY"

3. Check the database itself. In the Supabase SQL Editor:

-- tables with RLS off (rowsecurity = false) come first
select tablename, rowsecurity from pg_tables where schemaname = 'public' order by rowsecurity, tablename;

-- policies that let everyone through (check the roles column for anon or public)
select tablename, policyname, roles, cmd, qual, with_check from pg_policies where schemaname = 'public' and (qual = 'true' or with_check = 'true');

-- what anon and authenticated can touch (tables and views)
select table_name, grantee, string_agg(privilege_type, ', ' order by privilege_type) as privileges
from information_schema.role_table_grants
where table_schema = 'public' and grantee in ('anon', 'authenticated')
group by table_name, grantee order by table_name, grantee;

To close a gap on a table that should be private (swap reports and user_id for yours):

alter table public.reports enable row level security;
revoke all on table public.reports from anon, authenticated;
grant select, insert, update, delete on table public.reports to authenticated;
create policy "Users can read their own reports" on public.reports for select to authenticated using ((select auth.uid()) = user_id);
-- add insert, update and delete policies the same way (with check for insert and update)

Adding a policy doesn't remove a grant, which is why the revoke is there. More patterns, and what to do about views, are in Supabase RLS for vibe-coded apps and the view-bypass guide.

If a secret key leaked: rotate, then revoke

This is Supabase's order (Supabase: API keys). Don't skip steps, and don't retire the old key before the new one is live, or you'll take your own backend down. Supabase also says to delete a leaked key immediately, so don't leave the old one active longer than the steps need.

  1. Fix the root cause first. Remove the key from frontend code and from any VITE_ or NEXT_PUBLIC_ variable, or the next deploy ships the new key too.
  2. Create a new secret key (Dashboard, Settings, API Keys). You can create one per backend component, so a later leak only forces one rotation.
  3. Replace the old key everywhere your backend uses it. A legacy service_role key is replaced by a secret key.
  4. Confirm every component, including Edge Functions and scheduled jobs, now uses the new key.
  5. Then retire the old key. Deleting a secret key can't be undone, so wait until step 4 is true. Legacy anon and service_role keys are deactivated in the same Settings page, which Supabase says you can reverse.

Then assume anything the leaked key could read may have been read, and review what it could reach. A leaked legacy service_role key does not mean you must rotate your JWT secret: Supabase says that if the JWT secret is secure you can substitute a new secret key and avoid downtime. Revoking the legacy JWT secret is a separate step, and Supabase requires you to disable anon and service_role first, because both are JWTs signed with it. Rotating a signing key on its own doesn't revoke the old one; revoking does.

A visible anon or publishable key, on its own, needs no rotation. Moving from the legacy keys to the new ones is worth doing before the end-of-2026 deprecation: create the keys, swap the publishable key into client code, swap the secret key into backend code and Edge Functions, check nothing still uses the legacy keys, then deactivate them. See Supabase: migrating to new API keys. The key-handling basics for other providers are in exposed API keys in JavaScript bundles.

Paste-in fix prompt

For Lovable, Bolt, Cursor, Replit or any AI coding tool. Review what it changes before you accept it:

My app uses Supabase. Please do these in order and show me the changes and the SQL before applying anything:
1. Search the whole repo and any build output for service_role, sb_secret_, and any JWT whose role claim is service_role. Remove every one from frontend code and from env vars prefixed VITE_, NEXT_PUBLIC_, REACT_APP_ or PUBLIC_. If you find one, tell me, because I must create a new secret key, deploy it, confirm nothing uses the old one, and only then retire the old one.
2. The browser client must use only the publishable key (sb_publishable_...) or the legacy anon key.
3. Anything that needs elevated access must run server-side (Edge Function or API route) and read a secret key from a server-only secret.
4. For every table and view in the public schema: list who can read and write it with the anon and authenticated roles. Enable RLS on every table, revoke all from anon and authenticated, grant back only what the app needs, and write owner-only policies using (select auth.uid()) = user_id. Views must use security_invoker = true.
5. Do not disable RLS, do not write using (true) policies for private data, and do not grant anything to anon unless the data is meant to be public.
Do not change unrelated code.

Then repeat check 2 against your published site.

What LatchDog does here (and doesn't)

LatchDog keeps watching your live app after launch. Starter ($19/month, up to 3 apps) and Pro ($39/month, up to 10 apps) re-scan it automatically every day and after every deploy (triggered by a deploy hook from your host or CI), and email you only when something changes. Nothing to install: you verify your domain once. What that means for Supabase keys:

Also useful alongside this: how to monitor a Supabase app for leaked keys and open RLS, is my Lovable app secure? and the vibe coding security checklist.

Sources

Read directly on 2026-10-29: API keys, Securing your data, Row Level Security, Securing your API, JWT signing keys, Migrating to new API keys, the JWT signing keys announcement. Supabase changes these pages, so check them if something here no longer matches your dashboard.

Keep watching your app, not just checking it once

LatchDog keeps watching your live app after launch. Starter ($19/month, up to 3 apps) and Pro ($39/month, up to 10 apps) re-scan it automatically every day and after every deploy (triggered by a deploy hook from your host or CI), and email you only when something changes. Nothing to install: you verify your domain once.

Start with the free check (headers, HTTPS, leaked keys in your JavaScript, source maps, .env/.git). Open-table checks run after you verify your domain. See plans

Free, read-only public check: no sign-up, no card. Headers, HTTPS, leaked keys in your JavaScript (always masked), public source maps and exposed .env/.git files. Supabase table checks run after you verify you own the domain.

Want the open-table check too? Get my free deep scan: sign in with your email (no password) and verify you own the domain. Your first deep scan is free and adds CORS, more of your JavaScript and, if your app uses Supabase, whether anyone can read its tables and views with your public key.