Is It Safe to Expose Your Supabase Anon Key? Yes · With One Condition
The Supabase anon key is designed to be public, and exposing it is fine · as long as Row Level Security is on. Here's how to tell which key you actually shipped.
By Daniel A · Kraftwire Software
· 6 min readKey Takeaway
Yes, exposing your Supabase anon key is safe · it is designed to be public, and every Supabase-powered frontend ships it. The one condition: Row Level Security must be enabled with least-privilege policies on every table, because the anon key grants exactly what your RLS allows. The service_role key is the opposite case entirely · if that one ships client-side, treat it as a live breach and rotate it now.
The Two Keys · And Why They Are Nothing Alike
Every Supabase project issues two API keys, and almost every scare story about "leaked Supabase keys" comes from conflating them.
The anon key
The anon key is a JWT · a signed token whose payload contains "role": "anon". When your app sends it, PostgREST assigns the request the anon database role, the lowest-privilege role in the system. The key identifies your project and tags the request with that role. That is all it does. It is not a password, and hiding it buys you nothing: it sits in your JavaScript bundle, in every network request, and in the browser devtools of every visitor. Supabase's own documentation ships it in frontend examples because that is what it is for.
The service_role key
The service_role key is also a JWT, but its payload says "role": "service_role" · and that role bypasses Row Level Security entirely. It exists for trusted server environments: edge functions, backend jobs, migrations. Anyone holding it can read, modify, and delete every row in every table, no policies consulted. It must never appear in client code, a public repo, or a browser bundle. There is no "with conditions" here.
Same format, same place in the dashboard, opposite blast radius. Which is why the single most useful skill in this article is telling them apart.
Why Public Is Fine · When RLS Is On
Row Level Security is Postgres's per-row authorization layer. With RLS enabled on a table, every query · including ones made with the anon key · is filtered through policies that decide which rows the current role may see or touch:
Under this policy, a request with the anon key and no logged-in user sees nothing. A logged-in user sees only their own rows. The anon key being public is irrelevant, because the key was never the security boundary · the policies are. This is the whole design: publishable key in the client, enforcement in the database. If the policy patterns are new to you, RLS policies explained walks through them from zero.
The Real Risk: Tables Without RLS
Here is the failure that generates the horror stories. RLS is per table, and a table with RLS disabled answers to anyone who holds your project URL and anon key · both of which, as established, are public. The attacker does not need your app. One request does it:
If profiles has no RLS, that returns every row in the table. AI app builders made this failure common enough to earn a CVE: the pattern of missing RLS across Lovable-generated apps was catalogued as CVE-2025-48757, with the same anatomy every time · anon key public (fine), RLS absent (fatal).
Weak policies count too
Enabling RLS with a policy of using (true) for all operations re-opens the door politely. Policies need to be least-privilege:
- Reads scoped to the owner or to genuinely public data.
- Writes scoped to the owner, with
with check clauses so users cannot insert rows as someone else.
- No broad
for all policies added just to make an error go away.
The Supabase security checklist covers the full pass: RLS on every table, policy review, storage buckets, and the auth settings around them.
How to Check Which Key You Shipped
This is the five-minute audit that answers the question in this article's title for your app.
Step 1: Find the key your frontend uses
Open your deployed app, open devtools, and look at the apikey header on any request to *.supabase.co · or search your bundled JavaScript for eyJ, the telltale start of a base64-encoded JWT.
Step 2: Decode it and read the role claim
A JWT is three base64url segments separated by dots, and the middle segment is readable by anyone · no secret required. Paste the key into our JWT debugger and look at the payload:
"role": "anon" · you shipped the right key. Your remaining homework is RLS, not the key.
"role": "service_role" · stop reading and rotate. Every table in your project is currently open to anyone who has loaded your app.
Newer Supabase projects may use the revised key format instead, where the name does the work: sb_publishable_... keys are the client-safe kind, sb_secret_... keys are not.
Step 3: Check how the key got there
The usual route for a service_role leak is an environment variable with a client-exposing prefix · VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_ · pointed at the wrong value. Anything with those prefixes is compiled into the bundle. Our guide to environment variables security covers which prefixes ship to the browser on each framework.
If You Shipped the service_role Key
Move in this order, and do not reverse steps one and two:
- Rotate the key in the Supabase dashboard. This invalidates the leaked one immediately.
- Update your server-side code · edge functions and backend jobs · with the new key.
- Fix the leak path so the new key does not follow the old one out the door.
- Assume the data was read and act accordingly: check logs, and if personal data was exposed, treat it as the incident it is.
The step-by-step version, including git-history cleanup, is in how to fix exposed API keys.
When NOT to Worry
- The anon key is visible in your bundle, network tab, or page source. Normal, by design, not a finding.
- A scanner or a well-meaning commenter flags "exposed Supabase key" without checking the role. Decode it first; anon is fine.
- The anon key appears in your public GitHub repo. Also fine, with RLS in place · it is the same key every visitor already gets. The service_role key in a repo is the emergency, not this.
The pattern worth internalizing: the key's visibility is never the question. The role claim and your RLS coverage are the question.
How to Verify in Thirty Seconds
You can hand-check a table with the curl request above, and you should · run it once logged out and once with a second test user's token, because policies that block strangers can still overshare between customers. But RLS coverage is per table, policies drift, and one forgotten create table undoes the audit. SimplyScan's free scan checks Supabase RLS directly · alongside exposed secrets, frontend issues, and speed · as part of 51+ checks across 14 categories, and it flags both missing RLS and a service_role key in your bundle.
Not sure where your project stands? Run a free security scan and get the answer for every table at once.