Guides · VibeVigil
How to Check if Supabase RLS Is Enabled (and Actually Working)
A plain-English checklist to see if Row Level Security is on for every Supabase table, find policies that let everyone in, and fix it with a copy-paste prompt for your coding agent. Works for Lovable, Bolt, v0 and Cursor apps.
If you built your app with Lovable, Bolt, v0 or Cursor, it probably talks to Supabase straight from the browser. That is normal. But it means one setting decides who can read and change your data: Row Level Security (RLS).
This guide shows you how to check it yourself. You don't need to be a developer.
The short answer
Open the SQL Editor in your Supabase dashboard and run:
select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;Any row where rowsecurity is false is a table with RLS turned off. Fix those first.
But "RLS is on" does not always mean "my data is safe". Keep reading for the other checks.
Why this matters
- Your Supabase anon (publishable) key sits in your frontend code. Anyone can find it. That is by design. Supabase expects RLS to protect your data, not the key being secret.
- Supabase's docs say a table in an exposed schema (like
public) without RLS can be read and written by any role that has access to it, and that includes the anon key. (Supabase: Row Level Security) - This is not a made-up risk. In 2025, CVE-2025-48757 described Lovable-built apps that leaked data because RLS was missing or too loose. (NVD: CVE-2025-48757)
Step 1: Look for the "Unrestricted" label
- Open your project in the Supabase dashboard.
- Go to Table Editor.
- Look at the list of tables. A table with RLS off shows an Unrestricted label. Its tooltip says the table "can be accessed by anyone via the Data API as RLS is disabled."
Write down every table with that label.
Step 2: Run the Security Advisor
Supabase has a built-in checker.
- In the dashboard, open Advisors → Security Advisor.
- Click to run it and read every item.
These are the warnings that matter most for RLS (Supabase: Database Advisors):
| Warning | What it means in plain English |
|---|---|
rls_disabled_in_public ("Table publicly accessible") | RLS is off. Anyone with your project URL can read or change this table. |
policy_exists_rls_disabled | You wrote rules, but RLS is off, so the rules do nothing. |
rls_enabled_no_policy | RLS is on, but there are no rules, so the API returns nothing. Usually a "my app shows no data" bug, not a leak. |
permissive_rls_policy | A rule like USING (true) lets everyone through. |
security_definer_view | A view skips RLS and can show rows people shouldn't see. |
rls_references_user_metadata | A rule trusts user_metadata, which users can edit themselves. |
sensitive_columns_exposed | A column that looks private (like an email or token) may be public. |
You can also run the same checks from the terminal with the Supabase CLI: supabase db advisors.
Step 3: Read your policies (on is not the same as safe)
RLS can be on and still let everyone in. Run:
select tablename, policyname, roles, cmd, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename;Look for these red flags:
qualorwith_checkis justtrue. That rule lets every user through.rolesis{public}or includesanonon a table with private data. That means logged-out visitors are allowed.- An
UPDATErule with nowith_check. A user could change a row so it belongs to someone else. - A rule that uses
user_metadata. Users can change that value.
A good rule for private data usually looks like "only the owner can see this row":
create policy "Users can read their own notes"
on public.notes for select
to authenticated
using ( (select auth.uid()) = user_id );Step 4: Check who has access at all (grants)
Supabase has two layers: grants (can this role touch the table at all?) and policies (which rows?). Adding policies does not remove the default grants. Run:
select table_name, grantee, privilege_type
from information_schema.role_table_grants
where table_schema = 'public'
and grantee in ('anon', 'authenticated')
order by table_name, grantee;If anon has DELETE, UPDATE or INSERT on a table that logged-out visitors should never change, remove it (see the fix below).
Coming soon: Supabase announced that starting October 30, 2026, new tables in existing projects will no longer get these grants automatically. You will need to add a grant in the same migration where you turn on RLS. Tables you already have keep their current grants, so you still need to check them. (Supabase changelog)
Step 5: Check views and database functions
Two things can skip RLS even when every table is protected:
Views. By default, a Postgres view runs with the rights of the person who created it, so it ignores RLS. Find your views:
select c.relname as view_name, c.reloptions
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'v';If reloptions does not include security_invoker=true (or on), the view skips RLS. On Postgres 15 and newer you can fix it with:
alter view public.my_view set (security_invoker = true);Functions marked security definer. Functions in the public schema can be called through your API at /rest/v1/rpc/.... If one is security definer, it runs with full rights. Make sure logged-out visitors can't call it:
revoke execute on function public.my_function from public, anon;Step 6: Test your own app like a stranger
Do this only on an app you own.
- Open your app in a private (incognito) window. Don't log in.
- Visit every page. Does any private data show up?
- Create two test accounts. Log in as account A and add some data. Log in as account B. Can B see or change A's data?
If anything leaks, go back to Steps 3–5.
If you use Lovable
Lovable has a Security view. A quick scan runs on every publish and checks the database for tables without RLS and rules that let everyone through. You can also run a deep scan on demand. Lovable's own docs say these scans don't guarantee your app is fully secure, so still do Steps 3–6. (Lovable docs: Security)
How to fix it
Option A: fix it yourself in SQL
Replace notes and user_id with your table and owner column:
-- 1. Turn RLS on
alter table public.notes enable row level security;
-- 2. Remove access you don't need, then give back only what's needed
revoke all on table public.notes from anon, authenticated;
grant select, insert, update, delete on table public.notes to authenticated;
-- 3. One rule per action: users only touch their own rows
create policy "notes_select_own" on public.notes for select to authenticated
using ( (select auth.uid()) = user_id );
create policy "notes_insert_own" on public.notes for insert to authenticated
with check ( (select auth.uid()) = user_id );
create policy "notes_update_own" on public.notes for update to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
create policy "notes_delete_own" on public.notes for delete to authenticated
using ( (select auth.uid()) = user_id );If some data really is public (like a blog post list), give anon only select on that table, and write a select rule that shows only the public rows.
Option B: paste this prompt into your coding agent
Works in Lovable, Bolt, v0, Cursor or Claude Code:
Audit my Supabase database security and fix it. Do not delete any data.
1. List every table in the public schema and say whether Row Level Security is enabled.
2. Enable RLS on every table that doesn't have it.
3. For each table, tell me who should be able to read, insert, update and delete rows
(logged-out visitors, any logged-in user, or only the row's owner). Ask me if unsure.
4. Revoke the default grants from anon and authenticated, then grant back only what each table needs.
5. Write one policy per operation using "to authenticated" and "(select auth.uid()) = user_id".
Every UPDATE policy must have both USING and WITH CHECK. Never use USING (true) on private data.
Never use user_metadata in a policy.
6. Make every view in public use security_invoker = true.
7. Revoke EXECUTE from anon and public on any SECURITY DEFINER function that visitors shouldn't call.
8. Confirm the service_role / secret key is not used anywhere in frontend code or VITE_/NEXT_PUBLIC_ variables.
9. Put all changes in one migration file and explain each change in plain English.
10. Tell me how to test it: logged out, and with two different user accounts.After the fix, run the Security Advisor again (Step 2) and repeat the stranger test (Step 6).
FAQ
Is it OK that my Supabase anon key is visible in my code? Yes. The anon (publishable) key is meant to be public. RLS is what protects your data. The service_role (secret) key is different: it skips RLS completely and must never be in your frontend.
I turned on RLS and now my app shows no data. Why? RLS with no policies blocks everything. That's safe, but your app needs rules. Add select rules for the data each user should see (Step 3).
My AI fixed an RLS error by writing USING (true). Is that OK? Usually not. That rule lets everyone through, which is the same as having no protection for that action. Replace it with an owner check like (select auth.uid()) = user_id.
What changes on October 30, 2026? For existing Supabase projects, new tables in public won't be reachable from the API until you add a grant. Old tables keep their access, so check them with Step 4.
Lovable says my app passed its security scan. Am I done? It's a good start. Lovable itself says its scans don't guarantee full security. Do the policy, grants, view and stranger checks above too.
Sources
- Supabase docs, Row Level Security: https://supabase.com/docs/guides/database/postgres/row-level-security
- Supabase docs, Database Advisors (lints): https://supabase.com/docs/guides/database/database-linter
- Supabase changelog, breaking change for table grants (Oct 30, 2026): https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically
- Lovable docs, Security: https://docs.lovable.dev/features/security
- NVD, CVE-2025-48757: https://nvd.nist.gov/vuln/detail/CVE-2025-48757
Scan your app free. It checks the public page of a site you own. No account.