Supabase RLS: Is Your AI-Built App Leaking Data?
AI-built apps often ship with weak Supabase row-level security. How to check yours in five minutes, and what a correct policy looks like.
On this page · 5 sections
Key takeaways
- An app built on Supabase talks to the database straight from the browser. Row-level security (RLS) is the only thing stopping one user from reading everyone’s data.
- When researchers scanned 1,645 Lovable projects in 2025, 170 of them had RLS gaps that exposed data to anyone.
- You can test your own app in five minutes with the public key and one request. If it returns rows that aren’t yours, fix it before anything else.
Most apps built with Lovable, Bolt and similar AI tools use Supabase as their backend. It is a good choice: a real Postgres database, authentication and an API out of the box. But it comes with one rule that the AI tools don’t always follow, and when they don’t, the result is not a bug. It is a data leak.
This post explains that rule, how to check whether your app follows it, and what a correct policy looks like. It is part of the guide to making an AI-built prototype ready for real users.
Why does the database need row-level security?
Because your app’s frontend talks to it directly. A Supabase app ships a publishable key (older projects call it the anon key) inside the JavaScript that every visitor downloads. That is by design: Supabase says the publishable key is safe to expose, and adds that “Row Level Security decides what this client can reach, so enable it on every table before you deploy” (Supabase).
In other words, the key only identifies your project. What a visitor can read or change is decided entirely by RLS policies on each table. Without them, Supabase’s documentation is blunt: “A table in an exposed schema without RLS is readable and writable by any role with a grant on it.”
How common is the problem?
Common enough to get a CVE. In March 2025, researcher Matt Palmer scanned 1,645 projects built with Lovable and found 170 of them, about 10%, with inadequate RLS: 303 exposed endpoints, found only by looking at the projects’ homepages (statement on CVE-2025-48757). The exposed data included names and emails, API keys for third-party services, and payment and subscription details (CVE-2025-48757).
Lovable has since added a security scanner, but the underlying risk is not specific to one tool. Any app where the browser talks to Supabase depends on the policies being right, and AI tools write those policies the same way they write everything else: until the app works.
How do you check your app in five minutes?
1. List the tables without RLS. In the Supabase SQL editor, run:
select tablename, rowsecurity
from pg_tables
where schemaname = 'public';Any row with rowsecurity = false is a table anyone with your public key can read and write.
2. Try it as a stranger. Take the publishable key from your site (it is in the JavaScript) and request a table directly, without logging in:
curl 'https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*' \
-H "apikey: YOUR_PUBLISHABLE_KEY"If this returns other people’s profiles, so can anyone else’s request.
3. Try it as another user. Log in as user A, copy the ID of a record that belongs to user B, and ask for it. You should get nothing back.
4. Read the Security Advisor. Supabase’s dashboard flags tables without RLS and other common mistakes. It won’t catch a policy that is too permissive, which is why steps 2 and 3 matter.
What does a correct policy look like?
For a table where each row belongs to one user, the pattern is: RLS on, and one policy per action that compares the row’s owner with the logged-in user.
alter table public.projects enable row level security;
create policy "Owners read their projects"
on public.projects for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Owners create their projects"
on public.projects for insert
to authenticated
with check ((select auth.uid()) = user_id);Add update and delete policies the same way, and put an index on user_id. Supabase’s own guidance is to index the columns your policies use and to wrap auth.uid() in a select, which in its tests cut a query from 179 ms to 9 ms (Supabase).
Which mistakes should you look for?
using (true). A policy that allows everything is the same as no policy. AI tools write it to make an error go away.- RLS switched off to “fix” the app. Turning RLS on without policies blocks all access through the API, so the app breaks. The quick fix that follows is often to turn it off again.
- Insert and update without
with check. Users can then create rows that claim to belong to someone else. - Roles stored where users can edit them. An
is_admincolumn on the user’s own profile row, with an update policy that lets the user edit their row, makes anyone an admin. - The secret key in the frontend. Supabase’s secret (service role) key bypasses every policy. If it is in the browser, RLS doesn’t matter. More on that in the next post of this series.
Frequently asked questions
Is the Supabase anon key safe to put in my frontend?
Yes, the publishable (anon) key is designed to be public. It is only safe because row-level security controls what it can reach, so every table exposed through the API needs RLS enabled and policies that match who should see each row.
How do I know if my Lovable app has the RLS problem?
Check that RLS is enabled on every table in the public schema, then request a table with only the publishable key and no login. If you get rows back that should be private, your policies are missing or too permissive.
Does enabling RLS break my app?
It can, at first. With RLS on and no policies, the API returns nothing, so pages that read that table go empty. That is the safe failure. The fix is to add policies for each action, not to turn RLS off again.