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
publicschema 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:
trueon anything that is not genuinely public.- Policies granted to
anonorpublicon personal data. insertorupdatepolicies without awith_checkthat 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 definerfunction exposed through the API runs with elevated rights. Make sure it checksauth.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:
- Enable Row Level Security on every table in the
publicschema. - For each table with a
user_id, add select, insert, update and delete policies that only allow rows whereauth.uid() = user_id. - Remove any policy that uses
trueon personal data. - Make sure the Supabase service role or secret key is never used in frontend code.
- 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.