← Back to blog

Is the Supabase anon key safe to expose in your app?

You opened the network tab, or someone else did, and there it is: your Supabase project URL and a long key sitting in plain sight inside your JavaScript bundle. The AI tool that built your app put it there. The panic question follows immediately, usually a few days before launch. Did I just publish my database?

The short version: the key is fine, and it is meant to be there. What is not fine, in the majority of real Supabase breaches, is the thing the key is supposed to be paired with. This post covers what the key actually grants, how to test in about two minutes whether your tables are open, and the four specific ways Row Level Security can be switched on and still not protect anything.

Is the Supabase anon key safe to expose?

Yes. The Supabase anon key, and its modern replacement the publishable key, is designed to be embedded in client code. It identifies your project and marks the caller as unauthenticated. It is not a secret. It is only safe, however, when Row Level Security is enabled on every table in an exposed schema, because the key grants exactly what your policies allow.

Supabase says this plainly in its own documentation: the publishable key "is safe to expose with RLS enabled, because row access permission is checked against your access policies and the user's JSON Web Token." The conditional clause at the front of that sentence is the whole article. Nobody breaks a Supabase project by stealing the anon key. They find the anon key, which took no effort, and then discover the policies were never written.

This is the same shape as the Firebase security rules problem, where the client SDK config is public by design and the rules file is the only thing standing between a stranger and your collection. Backend-as-a-service moves authorization out of your server code and into a declarative layer, and that layer is easy to skip when your only test is whether the app works.

What can someone do with your anon key if RLS is off?

With RLS off, anyone holding your anon key can read, insert, update and delete every row in every table the Data API exposes. Supabase's documentation is direct about it: a table in an exposed schema without RLS is readable and writable by any role with a grant on it, and the default grants cover both the anon and authenticated roles.

That is not a theoretical read of the docs. It is the exact failure mode behind the Moltbook incident. On 2 February 2026, Wiz published research showing that Moltbook, a viral social network for AI agents, had shipped its Supabase URL and a sb_publishable_ key in a production JavaScript chunk with no RLS policies behind it. Roughly 4.75 million records were reachable without authentication, including 1.5 million agent API tokens, 35,000 email addresses from platform owners, 29,631 more from early access signups, and 4,060 private direct messages, some containing plaintext OpenAI keys that users had pasted to each other.

The disclosure timeline is worth sitting with, because it shows how long the fix takes once you know. First contact at 21:48 UTC on 31 January, the misconfiguration reported at 22:06, a first fix at 23:29, and after three more rounds (including the discovery that write access was still open) every table was secured by 01:00 UTC. Three hours of work, protecting data that had been public for as long as the app had been live. Moltbook's creator has said publicly that he did not write a line of the code himself.

The pattern predates Moltbook. In May 2025, security researcher Matt Palmer published CVE-2025-48757, an information disclosure issue affecting apps generated by Lovable through 15 April 2025, scored 8.26 and classed as CWE-863, incorrect authorization. The mechanism was identical: generated frontends talked to the database directly from the browser using the public anon key and relied entirely on RLS, so any generated app whose policies were missing or too loose let unauthenticated attackers read and, in some projects, write arbitrary tables. A scan of 1,645 Lovable projects found 170 leaking personal data, emails, financial information and API keys.

The anon key is not the vulnerability. It is the thing that makes the vulnerability trivially reachable by anyone who opens dev tools.

How is the anon key different from the service_role key?

The anon key resolves to a low-privilege Postgres role and is fully constrained by your RLS policies. The service_role key carries the BYPASSRLS attribute, so it ignores every policy you have written and can read and write everything. The anon key belongs in your frontend. The service_role key belongs only on a server you control.

Supabase is in the middle of replacing both, and if you started a project recently you may only ever have seen the new format. Four names are in circulation right now:

  • Legacy anon key. A long-lived JWT. Public by design, constrained by RLS. Being deprecated.
  • Legacy service_role key. A long-lived JWT that bypasses RLS entirely. Never ships to a client. Being deprecated.
  • Publishable key (sb_publishable_...). The replacement for the anon key. Same low privileges, same RLS behaviour, opaque rather than a JWT.
  • Secret key (sb_secret_...). The replacement for service_role. Servers, Edge Functions and cron jobs only.

Supabase has stated it is deprecating the anon and service_role keys by the end of 2026. Both systems work simultaneously until you deactivate the legacy pair in Settings, so you can migrate one client at a time. Two properties of the new keys are worth knowing even if you have not migrated. First, a secret key does not work in a browser at all: Supabase matches on the User-Agent header and returns HTTP 401, a guardrail the old service_role JWT never had. Second, a leaked secret key can be revoked in seconds without invalidating every signed-in user's session, which was the genuinely painful part of rotating service_role.

None of that changes the threat model. Moltbook leaked a sb_publishable_ key, the new and correct kind, and lost 4.75 million records anyway. Key hygiene and authorization are separate problems, and only one of them is solved by better key formats. If a service_role or secret key is what ended up in your bundle, that is a different and more urgent situation, covered in how apps leak API keys.

How do you check whether your Supabase tables are exposed?

Run one SQL query to list which tables have RLS enabled, then make an unauthenticated request against a table that should be private and confirm it fails. The SQL tells you what is configured. The request tells you what a stranger actually gets, which is the only answer that counts. Both take under two minutes.

In the SQL editor, list every table in the public schema and its RLS status:

select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;

Anything with rowsecurity false is open. Then list the policies that exist, because RLS enabled with zero policies is a different state again:

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

Now the test that matters. Take your publishable or anon key straight out of your own frontend bundle, and ask the REST endpoint for a table that should never be public:

curl "https://YOUR_PROJECT.supabase.co/rest/v1/profiles?select=*" \
  -H "apikey: YOUR_PUBLISHABLE_KEY"

An empty array or a permission error is what you want. Rows are a finding. Repeat it for your most sensitive table, and repeat it with -X POST and a small JSON body, because read access and write access fail independently. Moltbook's team fixed reads first and only found the open write path forty minutes later.

Supabase also ships a Security Advisor, reachable in the dashboard, through supabase db advisors in the CLI, or through the Management API. It runs a fixed set of lint rules and returns deterministic findings, several of which map directly to the failures in this article: rls_disabled_in_public, rls_enabled_no_policy, policy_exists_rls_disabled, security_definer_view, permissive_rls_policy and sensitive_columns_exposed. Read all of them before launch, not just the criticals.

Four ways RLS is on and still not protecting you

A green checkmark next to RLS is not the end of the check. These are the states that pass a glance and fail a stranger.

Enabled with no policies at all. This one is safe, but it is worth understanding rather than panicking about. Postgres looks for a policy granting access, finds none, and denies everyone except roles that bypass RLS. Supabase puts it this way: once RLS is enabled, no data is accessible through the API using a publishable key until you create policies. If your app has broken since you enabled RLS, this is why, and the fix is to write the policy, not to switch RLS back off.

A policy that is always true. The fastest way to make an app work again after enabling RLS is a policy with a using (true) condition, and AI coding assistants reach for it constantly because it satisfies the immediate error. It also restores the exact access you just removed. Check the qual column from the policy query above for anything that does not reference auth.uid() or an equivalent ownership check.

A view that bypasses the tables underneath it. Postgres creates views as security definer by default, meaning the view runs with the permissions of whoever created it, usually the postgres superuser. A view over a protected table can therefore serve rows the table itself would refuse. On Postgres 15 and above, the fix is explicit: create view public.my_view with (security_invoker = true) as .... On older versions, revoke access from the anon and authenticated roles or move the view to a schema the Data API does not expose.

Policies that trust data the user controls. A policy keyed on a claim inside the user's own JWT metadata is only as trustworthy as that metadata, and in Supabase Auth the user_metadata object is user-editable. A policy reading a role or tier from it can be defeated by the user updating their own profile. Supabase flags this as rls_references_user_metadata for a reason. Authorization decisions belong on values the user cannot write.

Do you need to rotate the anon key after it leaks?

No. The anon key is public by design, so a leak is not an event and rotating it changes nothing about your exposure. Rotating a service_role or secret key that reached a client is urgent and non-negotiable. If your anon key is what alarmed you, the correct response is to audit your policies rather than to cycle the key.

The distinction is worth being firm about, because rotating the public key feels like doing something and does not reduce risk by any amount. Anyone can extract the key again from the next build in the same thirty seconds. What changes your exposure is a policy on every table.

A service_role or secret key is the opposite case. Treat it as burned the moment it is in a bundle, a public repository, or a commit that was ever pushed, and rotate before you do anything else. Removing it from the current file is not enough if it still sits in an earlier commit, which is the trap covered in how git history keeps leaking secrets. The new secret key format helps here: revocation is instant and does not sign out your users.

The check to run before you point a domain at it

In order, and none of these needs a security background:

  1. Run the pg_tables query and confirm rowsecurity is true for every table in the public schema.
  2. Run the pg_policies query and read every qual value. Anything that does not tie a row to a user is a finding.
  3. Curl your two most sensitive tables with the key from your own bundle, once for read and once for write.
  4. Check every view for security_invoker, or move it out of an exposed schema.
  5. Open the Security Advisor and clear the RLS and view lints.
  6. Search your bundle and your repository for service_role and sb_secret_. Neither belongs in anything a browser downloads.
  7. If the Data API is not used at all, turn it off in the dashboard rather than defending it.

The broader version of this list, covering auth, rate limits and deploy config as well as the database, is in the pre-launch security checklist for apps built with AI coding tools. If you want the reasoning behind why generated code lands in this state so reliably, the security risks of vibe coding covers the mechanism.

Scanning for this before you ship

You can run the whole list above by hand, and for one small app that is a genuinely reasonable evening. If you would rather have something read your source and hand you a ranked list first, the free IOnclad browser scanner runs a 167-rule subset entirely in your browser, with no signup and no email wall. Your code stays in the tab.

The IOnclad desktop app runs 24 scanners and 500+ checks across secrets and git history, dependencies, the OWASP web categories, session and auth, an API surface map, and a denial-of-wallet check. It returns one Ship-It verdict with the file, line and a fix for each finding. It does not promise your app is secure, and no scanner honestly can. What it produces is evidence for a decision you are about to make anyway.

Whichever route you take, the sentence to end on is the one Supabase wrote first. The key is safe to expose with RLS enabled. Everything expensive lives in those last two words.

Free, in your browser
IOnclad browser scanner

Check your app for exposed keys and the issues that stop a launch. No signup, no email wall, and your code never leaves the tab.

Keep reading
IOnclad Your Firebase Security Rules Are Open by Default Read → IOnclad Hardcoded Secrets: How Apps Leak API Keys Read → IOnclad Am I Safe to Ship? A Pre-Launch Security Audit Read →
Reading us on Google?
Add The IOn Project as a preferred source

One click on Google’s preferences page, and our articles show up more often in your Top Stories, AI Overviews, and AI Mode.