Your app shows data to people who aren't logged in (Supabase RLS)
One error, one plain-English fix you can paste into your AI tool. Works for Lovable, Bolt, Cursor, Replit, or any app that talks to Supabase from the browser.
Last checked against official docs: 19 Oct 2026. This is educational information, not a security audit.
Symptom
- A logged-out visitor (or a different user) can see rows that should be private: other people's profiles, orders, messages.
- Or the reverse: after you "turn on security", your app suddenly shows empty lists or errors like
42501 permission denied.
Why it happens (plain words)
Your app's browser code talks directly to your database using a public key. That key is meant to be public: anyone can read it in the page source. The only thing that decides who sees which rows is Row Level Security (RLS): rules that live inside the database.
- A table with RLS off is readable and writable by anyone holding the public key (Supabase docs: "A table in an exposed schema without RLS is readable and writable by any role with a grant on it").
- A table with RLS on but a rule like
using (true)is also open to everyone that rule applies to. A security scanner can show it as "has a policy" while it protects nothing. - Views skip RLS by default unless created with
security_invoker = true.
How to confirm (5 minutes, no code)
- Open your live app in a private/incognito window, not logged in.
- Open DevTools (F12) → Network tab → reload. Filter for
rest/v1. - Click a request and look at the Response. If you see real rows of private data while logged out, you have a leak.
- In Supabase dashboard → Database → Tables (or Lovable: More → Cloud → Database → RLS policies): look for tables with RLS disabled, and policies whose condition is just
true. - Run the Security Advisor in Supabase (and Lovable's Security view if you use Lovable). Treat its output as a starting list, not proof you're safe.
Step-by-step fix
- List your tables and decide for each: public (anyone can read), personal (only the owner), team/shared, or server-only.
- Enable RLS on every table in the
publicschema:alter table public.your_table enable row level security; - Tighten grants (on projects that give
anon/authenticatedeverything by default; adding policies does not remove grants):revoke all on table public.your_table from anon, authenticated; grant select, insert, update, delete on table public.your_table to authenticated; - Write one policy per operation, always naming the role. For a table where each user owns rows via
user_id:create policy "own rows: select" on public.your_table for select to authenticated using ( (select auth.uid()) = user_id ); create policy "own rows: insert" on public.your_table for insert to authenticated with check ( (select auth.uid()) = user_id ); create policy "own rows: update" on public.your_table for update to authenticated using ( (select auth.uid()) = user_id ) with check ( (select auth.uid()) = user_id ); create policy "own rows: delete" on public.your_table for delete to authenticated using ( (select auth.uid()) = user_id ); - Replace any
using (true)on private tables. Keep it only for data that is genuinely meant to be public. - Views: recreate them
with (security_invoker = true)(Postgres 15+). - Don't use
user_metadatafor permissions. Users can edit it themselves. Useapp_metadataor a roles table. - Add an index on
user_id(or whatever column your policies filter on).
Copy-paste prompt for your AI tool
Audit my Supabase database security. For EVERY table in the public schema:
1. Tell me if Row Level Security is enabled, and list each policy with its role and condition, in a table.
2. Flag any table with RLS disabled, any policy using `true` on non-public data, any view without security_invoker = true, and any policy that reads user_metadata.
3. Propose a SQL migration that: enables RLS; revokes all from anon and authenticated and grants back only what the app needs; creates separate select/insert/update/delete policies scoped `to authenticated` using (select auth.uid()) = user_id (or the right ownership column); keeps genuinely public tables readable.
4. Do NOT run anything yet. Show me the SQL and explain in plain English what each table will allow for a logged-out visitor, a logged-in owner, and a different logged-in user.
Do not use the service_role/secret key in any browser code.
Read the proposed SQL before applying it. If you don't understand a line, ask the AI to explain that line.
How to verify it's fixed
- Repeat the incognito Network test: private tables should return
[]or an error, not data. - Log in as user A, create a record. Log in as user B (second account) and confirm B cannot see, edit, or delete A's record.
- Confirm public pages still load for logged-out visitors.
- Optional (more technical): Supabase recommends pgTAP tests per table run with
supabase test db.
Safety notes
- Back up / export data before changing policies on a live app.
- If data was exposed, assume it may have been copied. Depending on what it was and where your users are, you may have legal obligations (e.g. GDPR breach rules in the EU). Get proper advice.
- A green scanner result is not a guarantee. Scanners check that rules exist; they may not check that the rules are correct.
- Your AI tool can write a policy that "works" by opening everything. Always do the two-account test.
Sources
- Supabase: Row Level Security, https://supabase.com/docs/guides/database/postgres/row-level-security
- Supabase: API keys, https://supabase.com/docs/guides/api/api-keys
- Lovable: Security overview, https://docs.lovable.dev/features/security
- Lovable: Security best practices, https://docs.lovable.dev/tips-tricks/security-best-practices
- CVE-2025-48757 statement (researcher), https://mattpalmer.io/posts/2025/05/statement-on-CVE-2025-48757/ (Lovable disputes the CVE)
