Skip to content

Supabase anon key vs service role key: what each opens

View as MarkdownOpen in ClaudeOpen in ChatGPTOpen in Perplexity

The question is copied too. If the chat opens empty, paste it.

TL;DR

The anon key, now called the publishable key, is meant to be public: it maps to the anon or authenticated role, and your RLS policies decide what it sees. The service role key, now the secret key, maps to service_role, which bypasses RLS entirely. On my local test table with RLS on and no policy, the public key got 0 rows and the secret key got all 3. Put the publishable key in the browser, keep the secret key on a server, and rotate it if it ever shipped.

I’m Denys, a full-stack and AI engineer. A leaked secret key is the first thing I look for when I take over a Supabase app, because with it every other protection is decoration. This article explains what each key opens, with the requests I ran to check it.

What is the Supabase anon key?

The anon key identifies your app, not your user. Every request from a browser or a mobile app carries it, so it has to be public: anyone can read it from the page source. A request with only this key runs as the Postgres role anon. When a user signs in through Supabase Auth, the same key travels with the user’s token and the request runs as authenticated. Your RLS policies are written for those two roles.

Supabase is renaming the keys. The API keys guide calls the new ones publishable (sb_publishable_…) and secret (sb_secret_…), and deprecates the legacy JWT-based anon and service_role keys by the end of 2026. The roles behind them stay the same.

Key Legacy name Postgres role RLS applies? Where it goes
Publishable sb_publishable_… anon key anon, or authenticated with a user token Yes Browser, mobile app, anything you ship
Secret sb_secret_… service_role key service_role No, the role has BYPASSRLS Server, Edge Function, cron job only

Is it safe to expose the Supabase anon key?

Yes, if every table, view and function it can reach is protected. The key itself grants nothing special; the grants and policies on your schema decide everything. I checked this on a local Supabase stack (CLI 2.109.1, Postgres 17.6) with one table of three fake orders:

Which Postgres role each key becomes. And whether your RLS policies see the request. Publishable key, signed out: Publishable key → role anon → RLS policies apply; Publishable key, signed in: Publishable key + user token → role authenticated → RLS policies apply; Secret key: Secret key → role service_role → RLS skipped.
Figure 1. From the Supabase API keys guide, checked on a local stack, 23 October 2026.
Table state Publishable key Secret key
RLS off All 3 rows, and an insert returned 201 Created All 3 rows
RLS on, no policy [] All 3 rows
RLS on, policy using (true) All 3 rows All 3 rows
No grant for anon 42501 permission denied, with a hint naming the missing GRANT All 3 rows

So the publishable key is exactly as safe as your weakest table. A table without RLS, or with a policy that allows every row, is readable by anyone who opens DevTools. That is the hole behind CVE-2025-48757, where 170 of 1,645 Lovable showcase apps served data to the public key.

What is the service role key, and why is it dangerous?

The secret key is the master key. It runs as service_role, which has the BYPASSRLS attribute, so no policy you write ever applies to it. In the last column of the table above, it read all three rows in every state. Supabase’s guide lists where it belongs: servers that run their own authorization checks, Edge Functions, background jobs and admin tools. Two details from the guide matter in practice:

  • The new secret key refuses browser requests: Supabase matches the User-Agent and returns 401. The legacy service_role JWT has no such check, so a leaked legacy key works from anywhere. My local stack, which has no such gateway check, answered a script with the secret key with all 3 rows.
  • A secret key bypasses RLS only when the request carries no user token. With a signed-in user’s token, the request runs under that user’s policies.

An Edge Function that uses the secret key has to check who is calling and filter by owner or company itself, because nothing below it will.

Where do you find the keys?

In the Dashboard, open your project’s Connect dialog for the URL and publishable key, or Settings → API Keys for every key, legacy ones included. Locally, supabase start and supabase status print a publishable key and a secret key. Keep them in environment variables: in Next.js, only the publishable key may carry the NEXT_PUBLIC_ prefix, because the bundler ships anything with that prefix to the browser.

What do you do if the service role key leaked?

Treat it as compromised: every row in every table was readable and writable by whoever found it.

  1. Create a new secret key, move your servers to it, then delete the leaked one. If the leak was a legacy service_role key, the way out is the move to the new keys: create a publishable and a secret key, switch the app, then disable the legacy keys in Settings → API Keys, where they otherwise stay valid.
  2. Search the built JavaScript and the Git history for sb_secret_ and service_role. Escape’s scan of 5,600 vibe-coded apps found 400+ exposed secrets, most of them in frontend bundles.
  3. Review what changed while it was out: new users, role changes, deleted rows.

The free Supabase security check does not read your frontend, but it reads the database side of the same problem: roles that bypass RLS, tables with RLS off, policies that allow every row and columns that look sensitive and are readable by anonymous visitors. To check what your public key reaches in practice, the steps are in vibe coding security risks, and the full list I run on an app is in my Lovable app security checklist.

Not sure which key your app ships?

Happy to take a look and compare notes. No pitch.

Get in touch

© 2026 Denys Zaichenko. Original article: https://madsalt.tech/blog/supabase-anon-key-vs-service-role-key/