Fix guides

Supabase view bypasses RLS: anyone can read or delete rows through it

A Postgres view runs with its owner's rights by default, so a view over a table with RLS can hand every row to anyone with your anon key. How to find risky views in a Lovable, Bolt, or Cursor app and lock them down with security_invoker.

What you are seeing

Why it happens

  1. In Postgres, a view checks permissions as the role that owns the view, not the role querying it. Views created in the SQL editor or by migrations are usually owned by postgres, which bypasses RLS, so the view reads every row.
  2. Views in the public schema are exposed through the Data API like tables. If anon or authenticated has privileges on the view, anyone holding the anon key (which ships in your frontend) can query it.
  3. Supabase historically granted anon and authenticated broad privileges on objects in public by default, so a new view was often reachable without anyone granting it on purpose.
  4. Simple views can also be written through. If the roles have INSERT, UPDATE, or DELETE on an auto-updatable view, a request through the view can change or delete the underlying rows, skipping RLS the same way reads do.

How to fix it

1. Find every view in public and who can use it

select c.relname as view_name,
       coalesce(array_to_string(c.reloptions, ','), '') as options,
       has_table_privilege('anon', c.oid, 'select') as anon_select,
       has_table_privilege('anon', c.oid, 'delete') as anon_delete,
       has_table_privilege('authenticated', c.oid, 'select') as auth_select
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('v','m')
order by 1;

2. Make each view respect the caller's RLS (Postgres 15+)

alter view public.your_view set (security_invoker = true);

-- when you create new views, set it up front:
create view public.your_view
  with (security_invoker = true) as
  select ...;

3. Revoke what the API roles should not have

-- most views only need reads, for signed-in users only
revoke all on public.your_view from anon;
revoke insert, update, delete on public.your_view from authenticated;
grant select on public.your_view to authenticated;

4. Handle materialized views separately

Materialized views (relkind m) do not support RLS or security_invoker at all. Move them to a schema that is not exposed through the API, or revoke access from anon and authenticated and read them only from server code.

5. Test as an outsider

curl 'https://YOUR_PROJECT.supabase.co/rest/v1/your_view?select=*' \
  -H 'apikey: YOUR_ANON_KEY'

# expect [] or a permission error, never real rows

6. Tell your AI tool the rule

Add this to your project rules (.cursorrules, Lovable knowledge, or Bolt prompt): "Every view in public must be created WITH (security_invoker = true), and grant only SELECT to authenticated unless logged-out access is required. Never grant insert, update, or delete on views to anon."

How to know it is fixed

Re-run the query from step 1: every view shows security_invoker=true in options, and anon_select and anon_delete are false unless you meant the view to be public. The Security Advisor no longer lists security_definer_view, the curl test with the anon key returns no real rows, and a signed-in user sees only their own rows through the view. Sources: PostgreSQL CREATE VIEW and Supabase lint 0010.