Is the Supabase anon key safe to expose?

By the RowLocked team · Updated

Short answer: yes, the anon key (now called the publishable key) is designed to be public, but only if Row Level Security is set up correctly. The key itself is not the protection. Your RLS policies are.

This question comes up constantly from people building with Lovable, Bolt, Cursor or v0, usually after they open developer tools, find a long eyJ... string in their JavaScript, and wonder whether they have leaked something. This guide explains what the key can and cannot do, the one situation where seeing it should worry you, and how to check your setup in a few minutes.

What the anon key actually is

When your frontend creates a Supabase client, it needs two things: your project URL and an API key.

import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  "https://your-project.supabase.co",
  "sb_publishable_..." // or the legacy anon JWT, "eyJ..."
);

Because this code runs in the visitor's browser, both values are downloaded by every visitor. There is no way to hide them: minifying, obfuscating or loading them from an environment variable at build time still puts the literal string into the bundle.

Supabase knows this, and designs the key accordingly. The anon/publishable key carries the low-privilege anon Postgres role. Every request made with it is subject to Row Level Security. When a user signs in, the client sends their own access token alongside the key, and requests then run as the authenticated role with auth.uid() set to that user's ID, again subject to RLS.

Supabase's documentation describes the publishable key as safe to expose online: in web pages, mobile apps, and source code. It compares it to a key taped to the front door that only opens the lobby.

Publishable vs secret, anon vs service_role

There are two generations of keys, and you may see either:

PurposeNew keyLegacy keyRespects RLS?Where it belongs
Public client accesssb_publishable_...anon JWTYesBrowser, mobile app, anywhere
Full admin accesssb_secret_...service_role JWTNo, bypasses itServers and Edge Functions only

The table above is the whole story in one picture. If what you found in your frontend is a publishable or anon key, that is expected. If it is a secret or service_role key, you have a real problem; go straight to Supabase service_role key exposed.

To check a legacy JWT, decode its middle section and look at the role claim. You can do this locally without sending the key anywhere:

echo 'PASTE.JWT.HERE' | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
# {"iss":"supabase","ref":"...","role":"anon",...}   <- fine
# {"iss":"supabase","ref":"...","role":"service_role",...}   <- rotate now

When a public anon key becomes a real problem

The anon key is only as safe as your database policies. It becomes a door to your data when any of these is true:

  1. A table in the `public` schema has RLS disabled. Then anyone with the key can read and often write the whole table. Supabase's Security Advisor flags this as an error. See RLS Disabled in Public.
  2. RLS is enabled but a policy is too broad. A select policy of using (true) on a table of user data, or a policy that checks something the user controls, gives the same result as no RLS.
  3. A view or function bypasses RLS. Views created as a privileged user run with that user's permissions unless you set security_invoker. Functions marked security definer can do the same. See security definer views.
  4. A Storage bucket is public but holds files that should be private. Public buckets serve any file to anyone with its URL. See Supabase storage bucket security.
  5. Signups are open and "authenticated" is treated as "trusted". If anyone can sign up, a policy like to authenticated using (true) means "any stranger with a free account". Scope policies to the user's own rows.

Notice that none of these are about the key. Rotating the anon key does not fix any of them; a new key would be just as public.

How to check your setup in five minutes

1. Look at the Security Advisor. In the Supabase dashboard, open Advisors, then Security Advisor. Fix anything marked as an error first, especially RLS disabled, security definer views, and sensitive columns exposed.

2. List RLS status and policies. In the SQL Editor:

-- Tables in public and whether RLS is on
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;

-- Every policy, so you can read what each one allows
select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename, cmd;

Read each qual (the using expression). For tables of user data you want to see something like (auth.uid() = user_id), not true.

3. Test as the public. The SQL Editor bypasses RLS, so test with the actual public key from a terminal:

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

For a private table, you want an empty array [] or a permission error. If real rows come back, so would they for anyone. RowLocked's free scan runs a safer version of this check for every table it can find, asking only for a row count so no data is downloaded.

4. Test as a different user. Sign up a second test account and confirm it cannot see or edit the first account's data through your app.

What about rate limiting and abuse?

Even with perfect RLS, a public key lets anyone call your API. That is normal for any public web app, but a few settings reduce abuse:

  • Turn on email confirmation and CAPTCHA protection for signups in Auth settings if bots are creating accounts.
  • Keep expensive operations (AI calls, emails, payments) in Edge Functions that check the caller and apply your own limits, rather than in database triggers anyone can fire with an insert.
  • Avoid granting insert to anon on tables unless signed-out visitors truly need to write, and add a with check that constrains what they can write.

A prompt to audit this with your AI tool

If your app was built with Lovable, Bolt or Cursor, you can ask it to review the setup. Paste this, then verify the result yourself with the queries above:

Audit how this app uses Supabase. 1) List every Supabase key used in frontend code and confirm it is the anon/publishable key, never service_role or a secret key. 2) For every table the app reads or writes, confirm Row Level Security is enabled and list each policy in plain English. 3) Flag any policy using "true" on user data, any view without security_invoker, and any public storage bucket holding private files. Propose SQL fixes but do not weaken any policy.

Frequently asked questions

Should I put the anon key in an environment variable?

Yes, for convenience and so you can switch projects, but understand that VITE_ or NEXT_PUBLIC_ variables are still bundled into the browser code. An environment variable is not a way to hide it, and that is fine for this key.

Can someone run up my Supabase bill with my anon key?

They can make requests just like any visitor, which is true of every public website. RLS limits what those requests can access. For costly operations, put them behind Edge Functions with your own checks and limits.

Is the Firebase API key the same idea?

Similar. The Firebase web config "apiKey" identifies your project and is meant to be in client code; Firebase Security Rules are what protect your data. See Firebase security rules are public.

Do I need to rotate the anon key if it is in a public GitHub repo?

Not because of the repo; it was already public in your site's JavaScript. If you want to migrate to the newer publishable key anyway, you can create it alongside the legacy key and switch clients over gradually.

Related guides

All guides

Check my app now