Skip to content
✕PreflightX Scan your app — free
  1. PreflightX
  2. Guides
  3. Is your Supabase database public? Check Row Level Security

PREFLIGHTX / SECURITY GUIDES

Is your Supabase database public? Check Row Level Security

Your Supabase public key ships to every browser by design. Row Level Security decides what it can read. Check yours in five minutes and fix it.

Published September 27, 2026 · Updated September 27, 2026 · By the PreflightX team

On this page
  1. Why the public key is not the problem
  2. Why AI-built apps get this wrong so often
  3. Step 1: list tables without RLS
  4. Step 2: look for policies that are too generous
  5. Step 3: turn RLS on and scope it to the owner
  6. Step 4: check the places RLS does not cover by itself
  7. Step 5: test it from the outside
  8. A checklist you can paste into your AI builder
  9. Frequently asked questions

Check your live app in about a minute. Read-only, no signup, every finding free.

Scan my app — free

If your app was built with Lovable, Bolt, Cursor or v0 and talks to Supabase, your project URL and your public key are inside the JavaScript every visitor downloads. That is normal. Supabase is designed that way.

What is not normal is a table that answers that public key with every row. The only thing standing between the two is Row Level Security (RLS). In September 2026, UpGuard reported finding 16,326 Supabase databases exposing readable tables to the public web, and VibeEval's August 2026 scan found that 2,096 of 3,680 reachable Supabase-backed apps allowed unauthenticated table reads.

Short answer: open the Supabase dashboard, run the Security Advisor, and make sure every table in the public schema has RLS enabled and policies that only return the rows a user should see. Then test it from outside with the public key, exactly as a stranger would.

Why the public key is not the problem

Supabase gives every project two kinds of key:

Key Looks like Where it belongs
Public (anon / publishable) sb_publishable_… or a JWT with "role":"anon" In the browser. Safe to ship.
Secret (service role) sb_secret_… or a JWT with "role":"service_role" Server only. Never in frontend code.

The public key identifies your project. It grants whatever your database allows the anon role to do. With RLS on and sensible policies, that is almost nothing. With RLS off, it is everything in that table: read, and often insert, update and delete too.

The secret key bypasses RLS entirely. If it is in your frontend bundle, RLS cannot save you. Rotate it immediately (see what to do with exposed API keys).

Why AI-built apps get this wrong so often

Tables created in the Supabase Table Editor have RLS switched on by default. Tables created with raw SQL do not. AI coding tools tend to generate SQL migrations, so a create table statement lands without the enable row level security line, and nothing warns you in the app itself: everything works, because nothing is blocked.

The same thing happens with policies. A generated policy like this one makes a table readable by everyone, including people who are not signed in:

create policy "Enable read access for all users"
on public.profiles for select
using (true);

using (true) means "every row, for every request". Sometimes that is intended (a public list of blog posts). For profiles, orders, messages or anything personal, it is a leak.

Step 1: list tables without RLS

In the Supabase dashboard, open Advisors → Security Advisor. A table flagged RLS Disabled in Public is readable through your API by anyone holding the public key.

You can get the same answer in the SQL Editor:

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

Any row with rowsecurity = false needs attention.

Step 2: look for policies that are too generous

RLS on with no policies denies everything to the public key. That is safe, if unhelpful. The risk is policies that allow more than you meant:

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

Read every qual (who can see which rows) and with_check (what they may write). Watch for:

  • true on anything that is not genuinely public.
  • Policies granted to anon or public on personal data.
  • insert or update policies without a with_check that pins the row to the signed-in user.

Step 3: turn RLS on and scope it to the owner

For a table where each row belongs to one user (a user_id column), this is the common pattern:

alter table public.profiles enable row level security;

create policy "Users read their own profile"
on public.profiles for select
to authenticated
using ((select auth.uid()) = user_id);

create policy "Users update their own profile"
on public.profiles for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);

Add insert and delete policies only if the app really needs them, each pinned to auth.uid() the same way. Test your app afterwards: a feature that silently depended on an open table will now fail, which is exactly what you want to find before a stranger does.

Step 4: check the places RLS does not cover by itself

  • Views. A view runs with its owner's rights unless it is created with security_invoker = true, so it can bypass the RLS on the tables beneath it. The Security Advisor flags these.
  • Functions. A security definer function exposed through the API runs with elevated rights. Make sure it checks auth.uid() itself and takes no argument that lets a caller pick someone else's data.
  • Storage. Buckets have their own policies on storage.objects. A public bucket serves every file to anyone with the link. Keep user uploads in private buckets with owner-scoped policies.

Step 5: test it from the outside

The dashboard tells you what is configured. A request with the public key tells you what is actually exposed. On your own project only:

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

[] or a permission error means an anonymous visitor gets nothing from that table. A row of real data means anyone on the internet can read it, and your fix from step 3 has not landed yet.

Repeat for every table with personal data. Then test signed in as one user and try to read another user's row: RLS that lets any logged-in user read everything is still a leak.

A checklist you can paste into your AI builder

When you ask Lovable, Bolt or Cursor to fix this, be specific:

  1. Enable Row Level Security on every table in the public schema.
  2. For each table with a user_id, add select, insert, update and delete policies that only allow rows where auth.uid() = user_id.
  3. Remove any policy that uses true on personal data.
  4. Make sure the Supabase service role or secret key is never used in frontend code.
  5. Keep user uploads in private storage buckets with owner-scoped policies.

Then check the result yourself with steps 1 and 5. Generated fixes are a starting point, not proof.

Frequently asked questions

Is it safe that my Supabase URL and anon key are visible in the browser? Yes. They are designed to be public. Security depends on RLS and your policies, not on hiding the key.

Does enabling RLS break my app? It blocks every request that no policy allows. If a feature stops working, write a policy for exactly what that feature needs, rather than switching RLS off again.

Can PreflightX see my data? No. A PreflightX scan reads what any visitor can already download. If it finds a Supabase URL and a public key, it makes one bounded read of a common table with limit=1 to show whether anonymous access works. The row is never stored or shown to anyone, and PreflightX never uses a service role key.

Check what your app exposes right now

PreflightX runs a read-only scan of your live app's public surface: exposed keys in your JavaScript, readable Supabase tables, open storage buckets, deployed .env and .git files, source maps and missing security headers. Every finding is free to see.

Scan my app — free

More security guides

  • Vibe coding security statistics 2026: what scans found
  • Security headers for AI-built apps: CSP, HSTS and CORS
  • .env and .git exposed on your site? How to check and fix
  • Exposed API keys in JavaScript: safe to ship or rotate now?
  • Security checklist for Lovable, Bolt, Cursor and v0 apps
✕PreflightX

A safer internet starts with you.

GuidesPrivacyTermsRefundsAcceptable UseCookiesContact
© 2026 PreflightX