Supabase RLS in Lovable apps: how to check every table is actually protected
Row Level Security (RLS) is the only thing standing between your Lovable app's public Supabase key and your data. A table without it can be read and changed by anyone on the internet, no login required. Here is how to find those tables in two minutes, what the right policies look like, and the prompt that gets Lovable to write them.
What RLS is, in one paragraph
Row Level Security is a Postgres feature that attaches rules to a table saying which rows each user may see or change. Supabase relies on it completely: your app's anon key is public, every visitor has it, and it can run any query the rules allow. With RLS on and good policies, the anon key can only reach the rows the signed-in user owns. With RLS off, the anon key can read, insert, update and delete every row in the table.
Should you use RLS in Supabase? Yes, on every table in the public schema, without exception. A table that is genuinely public (a blog, a product catalogue) still gets RLS on, with a policy that allows SELECT for everyone and nothing else.
Why this matters more in Lovable apps
Lovable creates tables as you describe features. Describe a profile page and you get a profiles table. Describe comments and you get comments. Each one is written as a migration in the supabase/migrations/ folder of your repo. Lovable usually enables RLS and adds policies, but three things go wrong often enough to check for:
- A table is created and RLS is never enabled, usually when the migration was written in a hurry or edited by hand.
- RLS is enabled but the policy says
using (true), which passes for everyone. This is what "fix the permission error" prompts produce. - A later prompt breaks an earlier policy. You add a feature, Lovable rewrites a policy to make it work, and a rule that used to say "own rows only" now says "any signed-in user".
Lovable Cloud does not change any of this. It runs on Supabase, so the same tables, the same policies and the same checks apply.
Check 1: tables with RLS disabled
Open your project in the Supabase dashboard, go to the SQL editor, and run:
select tablename
from pg_tables
where schemaname = 'public'
and rowsecurity = false
order by tablename;
Every table listed is open to anyone with your anon key, which is anyone who has opened your app. An empty result is what you want. The Supabase dashboard also shows an "RLS disabled" warning on the Table Editor for each one, and the Security Advisor under Database lists them, so you can cross-check.
Check 2: policies that let everyone in
RLS on with no policies blocks everything, which breaks the app loudly and gets fixed. RLS on with a permissive policy breaks nothing and protects nothing, which is why it survives. Find those:
select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
and (qual = 'true' or with_check = 'true')
order by tablename;
Read each row and ask: should everyone be able to do cmd on this table? For SELECT on a public catalogue, yes. For SELECT on profiles, orders or messages, no. For INSERT, UPDATE or DELETE on almost anything, no.
Also look for roles = '{public}' on write policies. Writes should be restricted to authenticated at minimum.
Check 3: views that skip RLS
A view runs with the permissions of whoever created it (usually the owner, who bypasses RLS) unless it is created with security_invoker. Lovable sometimes creates views for dashboards. A view over profiles can show every profile even when the table itself is locked down.
select table_name
from information_schema.views
where table_schema = 'public';
For each view, check that its definition includes with (security_invoker = true), or that it only exposes data that is genuinely public.
What good policies look like
These are the patterns most Lovable apps need. Replace the table and column names with yours. The pattern is always the same: enable RLS, then write one policy per action that names exactly who is allowed.
A table where each user owns their row (profiles)
alter table public.profiles enable row level security;
create policy "Users read own profile"
on public.profiles for select
to authenticated
using (auth.uid() = id);
create policy "Users update own profile"
on public.profiles for update
to authenticated
using (auth.uid() = id)
with check (auth.uid() = id);
A table where rows belong to a user (todos, orders, notes)
alter table public.todos enable row level security;
create policy "Owner can read"
on public.todos for select to authenticated
using (auth.uid() = user_id);
create policy "Owner can insert"
on public.todos for insert to authenticated
with check (auth.uid() = user_id);
create policy "Owner can update"
on public.todos for update to authenticated
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
create policy "Owner can delete"
on public.todos for delete to authenticated
using (auth.uid() = user_id);
A genuinely public table (blog posts, a product list)
alter table public.products enable row level security;
create policy "Anyone can read products"
on public.products for select
using (true);
-- no insert/update/delete policies: only the service role (your server) can write
The Lovable prompt
You do not have to write the SQL. Tell Lovable the table and the rule. The important parts are naming the table, saying who may do what in terms of auth.uid(), and asking it to show you the policies so you can read them back.
Security fix: the Supabase table "todos" has Row Level Security turned off (or policies that allow everyone), so anyone with the public anon key can read and change every row.
Add a migration that enables Row Level Security on "todos" and adds policies so a signed-in user can only select, insert, update and delete rows where user_id = auth.uid(). Use with check on insert and update. No public access.
Do not change the UI. Confirm the todos page still works for a signed-in user, then show me the exact policies you created.
If Lovable comes back with using (true) anywhere, or suggests using the service_role key in the front end, say no and repeat the rule in plain words: "only the row's owner, identified by auth.uid()".
Catching it in the repo before it ships
Every table Lovable creates is a migration file in your GitHub repo, so the problem is visible in the code before it is visible in the database. A migration with create table and no matching enable row level security, or a policy with using (true) on a write, is a finding you can catch on push. That is what VibeProtect's repo scan does: it reads supabase/migrations/ on every push and flags each table without RLS, each policy that lets everyone in, and each view that bypasses RLS, with the Lovable prompt attached.
Quick reference
| Symptom | Likely cause | Fix |
|---|---|---|
| Table listed by Check 1 | migration never enabled RLS | alter table ... enable row level security, then add policies |
| Policy with qual = true on SELECT of private data | a "fix the permission error" prompt | replace with auth.uid() = user_id |
| Policy with with_check = true on INSERT or UPDATE | same | with check (auth.uid() = user_id) |
| App works with no policies at all | service_role key in the front end | remove it, rotate it, add real policies |
| Dashboard view shows other users' data | view without security_invoker | recreate the view with (security_invoker = true) |
Questions people ask
Should RLS be enabled on every Supabase table?
Yes, on every table in the public schema. Supabase exposes those tables through its API with your public anon key, and RLS is the mechanism that limits what that key can do. A table that should be readable by everyone still gets RLS enabled, with a SELECT policy that allows it and no write policies.
Is RLS disabled by default in Supabase?
Tables created through the Supabase dashboard's Table Editor have RLS enabled by default. Tables created by SQL or by a migration, which is how Lovable creates them, do not, unless the SQL says so. That is why checking the migrations matters.
Does enabling RLS break my Lovable app?
It will, briefly, if you enable RLS without adding policies: every query returns nothing. That is why the prompt asks Lovable to add the policies in the same migration and confirm the page still works. A broken page is better than an open table, and the fix is one more prompt.
What about Lovable Cloud instead of my own Supabase?
Lovable Cloud runs on Supabase, so every check and every policy here applies. The difference is where you open the SQL editor. The tables, the migrations in your repo and the mistakes are the same.
Is there a Supabase RLS checker that does this automatically?
Supabase's own Security Advisor lists tables without RLS in the dashboard. VibeProtect checks the migrations in your GitHub repo on every push, which catches the problem before the migration runs, and includes the policy problems (using true, views without security_invoker) that a table-level check misses.