Guide · Supabase

Supabase RLS: the #1 security bug in vibe-coded apps

Your anon key being public is fine. Missing Row Level Security behind it is how attackers read your entire database with a single request.

Normal, not a bug

Your Supabase anon key appearing in your JS bundle, browser devtools, or view-source. It is a public identifier by design — Supabase documents this. It is meaningless on its own.

Actually dangerous

A table with Row Level Security disabled, or a policy written too permissively. That is what turns a public anon key into a way to read or write your entire database, unauthenticated.

Why this happens in AI-built apps specifically

Cursor, Bolt, Lovable, and v0 all make it trivially easy to wire a frontend directly to Supabase — that speed is the entire appeal. What gets skipped in the rush from prompt to working demo is the authorization layer: RLS policies have to be written deliberately, per table, and nothing in the default scaffold forces that step. The app works perfectly in testing, because the person testing it is the person who created every row. It looks secure until someone else's anon key — the same public one anyone can grab from your own site — sends a request your policies were never written to block.

How to actually check, not just enable

The Supabase dashboard's "RLS enabled" toggle is necessary but not sufficient. A table can have RLS switched on and still be wide open if the policy attached to it evaluates to true for everyone. The only test that tells you what an actual attacker sees is an unauthenticated request against your live REST endpoint using nothing but your public anon key — the same request anyone visiting your site could make.

Tables worth checking first, because they are the ones AI scaffolds name predictably and the ones most likely to hold anything sensitive:

profilesusersorderspaymentssubscriptionsmessagesaccounts

The fix

Enable RLS on every table (it defaults to deny-all until you write a policy — safe by default), then write an explicit policy per operation you actually intend to allow:

-- Enable RLS on the table
alter table profiles enable row level security;

-- Only allow a user to read their own row
create policy "Users can view own profile"
on profiles for select
using (auth.uid() = id);

-- Only allow a user to update their own row
create policy "Users can update own profile"
on profiles for update
using (auth.uid() = id);

Repeat per table, per operation (select / insert / update / delete). A table with no policy after RLS is enabled denies everyone by default — that is the safe failure mode, and if your app breaks after enabling RLS, that is your app telling you a policy is missing, not a reason to leave RLS off.

Not sure if your RLS policies actually hold up?

StackSecured tests your live Supabase endpoint with real unauthenticated requests — not just a dashboard checklist — and tells you exactly which tables and columns are exposed.

Run a free scan

Common questions

Is it safe for my Supabase anon key to be visible in my JavaScript bundle?

+

Yes. The Supabase anon (publishable) key is designed to ship in client-side code — it is not a secret. It identifies your project to Supabase, nothing more. Seeing it in your bundle, browser devtools, or a security scan is expected behavior, not a vulnerability by itself.

So what actually makes a Supabase app insecure?

+

Row Level Security (RLS) being disabled or misconfigured on your tables. The anon key lets anyone send requests to your database as an unauthenticated user — RLS policies are what decide whether that unauthenticated user can actually read or write rows. No RLS policy (or a policy that defaults to allow) means anyone with your anon key, which is public by design, can query your tables directly.

How do I check if my Supabase RLS is actually enforced, not just enabled?

+

Enabling RLS in the Supabase dashboard without writing a policy defaults to denying all access — that is safe, but often means your app breaks instead of leaking data, which is how a lot of RLS bugs actually get caught. The dangerous version is a policy that exists but is written too permissively (e.g. USING (true)). The only reliable check is to make an actual unauthenticated request to your REST endpoint with just the anon key and see what comes back — reading the dashboard toggle alone does not tell you what an attacker sees.

What tables are most commonly left exposed in vibe-coded Supabase apps?

+

Bolt, Lovable, v0, and Cursor scaffolds converge on a small set of predictable table names — profiles, users, orders, payments, messages, subscriptions being the most common. These are exactly the tables worth checking first, because they are also the ones most likely to contain emails, addresses, or payment metadata.

More guides: Exposed API keys in AI-built apps · Full vibe-coding security checklist