Supabase view bypasses RLS? Fix it with security_invoker (and lock down SECURITY DEFINER functions)
Short answer: by default a Postgres view reads its tables with the view owner's privileges, and on Supabase that owner is usually postgres, which bypasses RLS. So a view in your exposed public schema can hand signed-out visitors rows your policies hide. On Postgres 15+, add security_invoker = true. Otherwise revoke select on the view from anon and authenticated, or move it to a schema the API doesn't expose. To see what your own live site gives away, run a free SniffPup scan (SniffPup is a free passive security scanner for vibe-coded apps). Only check sites and projects you own.
Home › Guides › Supabase view bypasses RLS · Last updated · By Ember, SniffPup
The fast way: a free SniffPup scan runs 15 checks from the outside, including Supabase read access using the public config your site already ships. No signup. Ember can sniff out an open table, but she can't see inside your database, so pair it with the SQL below.
A 100 means no issues were found in our checks. It is never proof of security.
1. Why a view skips your RLS policies
From the Postgres CREATE VIEW docs: by default, access to the tables a view reads is checked with the permissions of the view owner, and if those tables have row-level security, the view owner's policies are applied. Supabase's RLS guide puts it plainly: views bypass RLS by default because they are usually created with the postgres user. Anything in public is reachable through the Data API, so a view like create view public.order_summary as select * from orders can return every customer's orders to anyone with your public key.
Supabase's Security Advisor reports this as 0010 security_definer_view, level ERROR: "View bypasses row-level security."
2. The fix: security_invoker = true (Postgres 15+)
With security_invoker set, Postgres checks the tables with the permissions and RLS policies of the user running the query, as if they had queried the tables directly. New view:
create view public.order_summary with (security_invoker = true) as select id, user_id, total from public.orders;
Existing view (per ALTER VIEW, you must own it):
alter view public.order_summary set (security_invoker = true);
After this, the role calling the view also needs its own select grant on the underlying tables, and its RLS policies decide which rows come back. If the view suddenly returns a permission error or nothing, that is the table's grants and policies doing their job; fix them on the table, not by turning the option off.
3. The alternatives
- Revoke access: if the browser never needs the view, take it away from the API roles:
revoke select on public.order_summary from anon, authenticated;This is also what Supabase recommends on Postgres versions older than 15. - Move it out of the API: put the view in a schema that isn't listed under Exposed schemas in your API settings, for example
alter view public.order_summary set schema private; - Materialized views: these can't respect RLS at all. Supabase's advisor says to revoke
selectfrompublic,anonandauthenticated.
4. Find views in public without security_invoker
Run this in the SQL editor. Every row is a view that still checks tables as its owner:
select n.nspname as schema, c.relname as view,
pg_get_userbyid(c.relowner) as owner, c.reloptions
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.relkind = 'v'
and n.nspname = 'public'
and not exists (
select 1 from unnest(coalesce(c.reloptions, '{}')) o
where lower(o) in ('security_invoker=true', 'security_invoker=on',
'security_invoker=1', 'security_invoker=yes'))
order by c.relname;Then check who can read each one with select has_table_privilege('anon', 'public.order_summary', 'select');
5. Test it signed out (your own project only)
The REST API serves views at the same path style as tables (see Supabase's REST API docs, which cover tables and views alike). Call it with only your public key, no user session:
curl "https://YOUR_REF.supabase.co/rest/v1/order_summary?select=*&limit=1" \ -H "apikey: YOUR_PUBLISHABLE_OR_ANON_KEY"
- A permission error or an empty list: signed-out visitors can't read it. Right for private data.
- Rows you didn't mean to be public: the view is leaking. Apply section 2 or 3, then run the curl again.
6. The same trap in SECURITY DEFINER functions (RPC)
A function declared security definer runs as its owner. Supabase's database functions docs warn that one created in the Dashboard or by a migration is owned by postgres, bypasses RLS, and by default any role can call it, including anon. In an exposed schema it is callable at /rest/v1/rpc/<name> (advisor lint 0028). To lock it down:
- Prefer security invoker (the default) unless the function truly needs to skip RLS:
alter function public.my_fn(int) security invoker; - Pin the search path on every security definer function with
set search_path = ''and schema-qualify every name inside it. - Revoke execute from
publicandanon(andauthenticatedif signed-in users shouldn't call it):revoke execute on function public.my_fn(int) from public, anon; - Or keep it private: Supabase's RLS guide says never to create a security definer function in a schema listed under Exposed schemas; put helpers in a schema like
private.
List the security definer functions in public and their owners:
select p.proname, pg_get_function_identity_arguments(p.oid) as args,
pg_get_userbyid(p.proowner) as owner
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef
order by p.proname;More on tables themselves: Supabase RLS policy examples and is the Supabase anon key safe?
FAQ
Why does my Supabase view bypass RLS?
Because Postgres views check the underlying tables with the view owner's permissions by default, and the RLS policies applied are the owner's too. Views made in the Supabase SQL editor or by a migration are usually owned by postgres, which bypasses RLS, so the view can return every row. Supabase's Security Advisor flags this as security_definer_view (View bypasses row-level security).
How do I make a view respect RLS?
On Postgres 15 and above, set security_invoker = true: create view ... with (security_invoker = true) as select ..., or for an existing view, alter view public.my_view set (security_invoker = true). The view then checks the tables and their RLS policies as the user running the query.
What if my project is on an older Postgres version?
Supabase's docs say to protect the view by revoking access from the anon and authenticated roles, or by putting it in a schema the API doesn't expose.
Do materialized views respect RLS?
No. Supabase's advisor docs say materialized views can't be set to respect RLS, so if one is reachable by anon or authenticated, every row is visible. Revoke select from public, anon and authenticated.
Are SECURITY DEFINER functions risky too?
Yes. A security definer function runs as its owner, usually postgres, so it can return rows the caller couldn't read. In an exposed schema anyone with the public key can call it at /rest/v1/rpc/<name> unless you revoke execute from public and anon. Pin search_path = '' on it, prefer security invoker, or keep it in a schema the API doesn't expose.
Does a clean SniffPup report mean my views are safe?
No. A score of 100 means no issues were found in our checks. It is never proof of security: an outside scan doesn't list your views or functions, so run the SQL on this page and Supabase's Security Advisor too.
Sources: Supabase Row Level Security, Advisors (0010 security_definer_view, 0016, 0028), Database functions and REST API docs; Postgres CREATE VIEW and ALTER VIEW. Checked October 29, 2026.
A score of 100 means no issues were found in our checks. It is not proof that your app is secure.