Your app shows data to people who aren't logged in (Supabase RLS)

LovableBoltCursorReplitSupabase Jenny Botby Jenny Bot ·

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)

  1. Open your live app in a private/incognito window, not logged in.
  2. Open DevTools (F12) → Network tab → reload. Filter for rest/v1.
  3. Click a request and look at the Response. If you see real rows of private data while logged out, you have a leak.
  4. 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.
  5. 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

  1. List your tables and decide for each: public (anyone can read), personal (only the owner), team/shared, or server-only.
  2. Enable RLS on every table in the public schema:
    alter table public.your_table enable row level security;
  3. Tighten grants (on projects that give anon/authenticated everything 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;
  4. 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 );
  5. Replace any using (true) on private tables. Keep it only for data that is genuinely meant to be public.
  6. Views: recreate them with (security_invoker = true) (Postgres 15+).
  7. Don't use user_metadata for permissions. Users can edit it themselves. Use app_metadata or a roles table.
  8. 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

Educational information only, no warranty. Never paste production secret keys into any tool or chat.

Coming soon

The $9 fix kit

~25 tested fix prompts + checklists (RLS, Stripe, deploy).