Supabase "Security Definer View" warning: why your view bypasses RLS and how to fix it
By Pritesh Ghodge, security engineer · Last updated 2 November 2026
Find every view that ignores your RLS in about a minute.
Paste the output of one read-only SQL query into the free Supabase RLS policy checker. It flags views without security_invoker, materialized views, RLS that's off and using (true) policies. Nothing is uploaded. Or check your live app from the outside:
Weekly AI-app security checklist
One short email a week: a practical check for apps built with Supabase, Next.js and AI tools (RLS, leaked keys, headers and more). Unsubscribe anytime.
You turned on Row Level Security, wrote a policy so users only see their own rows, and Supabase's Security Advisor still shows a red Security Definer View error. Or your AI builder created a view like orders_with_customer to make a dashboard query easier, and nobody checked who can read it.
The short version: a normal Postgres view doesn't use the visitor's RLS. It reads the tables underneath with the permissions of the view's owner. In Supabase that owner is usually a role that bypasses RLS, so the view can hand every row to anyone who can query it, including visitors with only your public (anon) key.
Why a view bypasses RLS
Supabase's advisor docs for check 0010 put it plainly: Postgres' default for views is to run with the permissions of the view's creator rather than the querying user, a default kept for backwards compatibility. Anything in the public schema is reachable through your project's API, so a view there can expose more over the API than you meant.
Your policies on orders are still correct. They just aren't being checked against the visitor when the data comes through the view.
Two related cases behave the same way or worse:
- Materialized views store a copy of the data, and RLS never applies to them. Supabase flags these as check 0016.
- Views over
auth.usersare their own advisor error (0002) because they can expose your users' emails and metadata.
Check 1: list your views and who can read them
Run this in the SQL editor. It only reads the catalog:
select c.relname as view_name,
case c.relkind when 'v' then 'view' else 'materialized view' end as kind,
coalesce(c.reloptions::text[] && array['security_invoker=true','security_invoker=on','security_invoker=1'], false) as security_invoker,
has_table_privilege('anon', c.oid, 'select') as anon_can_read,
has_table_privilege('authenticated', c.oid, 'select') as signed_in_can_read
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;Any row where security_invoker is false and anon_can_read or signed_in_can_read is true needs a decision. Every materialized view readable by an API role does too. Remember that in most apps sign-up is open, so "signed in" means "anyone with an email address".
Also check your Postgres version with select version();. The security_invoker option needs Postgres 15 or later.
Check 2: what does a stranger actually get?
Only test your own project. Views are queried through the same REST endpoint as tables, so with the public key from your app:
curl -s "https://YOUR-PROJECT.supabase.co/rest/v1/YOUR_VIEW?select=*&limit=1" \
-H "apikey: YOUR_PUBLIC_KEY" \
-H "Authorization: Bearer YOUR_PUBLIC_KEY"[] or a permission error means a signed-out visitor gets nothing. A row of data means anyone can page through the view. Compare it with the same request against the underlying table: if the table returns [] but the view returns rows, the view is bypassing your RLS.
Fix: pick one per view
1. The view should respect RLS (most cases)
alter view public.orders_with_customer set (security_invoker = true);Or when you create it: create view public.orders_with_customer with (security_invoker = on) as select …. From then on, the policies and grants on the underlying tables are checked against whoever queries the view. Callers need SELECT on those tables, which Supabase's API roles usually have, and RLS decides which rows come back.
Then test as a signed-out visitor (check 2 again) and as two different signed-in users. If a page goes empty, the fix belongs in the table policies, not in turning the view back.
2. The view isn't meant for the API at all
Admin reports and internal rollups don't need to be reachable with your public key:
revoke select on public.monthly_revenue from anon, authenticated;Or move it to a schema that isn't exposed, such as private, and read it from server code with the secret key.
3. The view deliberately shows a public slice of private data
Sometimes the owner's rights are the point: a public_profiles view that shows display names from a table users can't otherwise read. If you keep it, make the view itself the boundary: select only the columns that are safe for everyone, never select *, and review it whenever the table gains a column. Supabase's own suggestion for user data is a separate profiles table holding just the public columns, with its own RLS.
4. Materialized views
revoke select on public.leaderboard_mv from public, anon, authenticated;Supabase's advisor docs note you need to revoke from the public role too, because default privileges in the public schema are granted there. Serve the data through a function or table with real checks if users need it.
The same trap in functions: security definer RPCs
Functions have the same switch. A function declared security definer runs as its owner and skips RLS, and Supabase's advisor (checks 0028 and 0029) points out that new functions in public are executable by anon and authenticated by default, reachable at /rest/v1/rpc/<name>. List yours:
select p.proname, pg_get_function_identity_arguments(p.oid) as args,
pg_get_userbyid(p.proowner) as owner,
has_function_privilege('anon', p.oid, 'execute') as anon_can_call
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef
order by 1;For each one, either revoke execute on function public.f(args) from anon, authenticated, public;, switch it to security invoker, or, if it's meant to be public (a contact form, a vote), make sure it validates its input and does only that one thing.
A prompt for Lovable, Bolt or Cursor
Audit the Supabase views and functions in this project:
1. List every view and materialized view in the public schema, with
whether it uses security_invoker and whether anon or authenticated
can select from it.
2. For views that should follow the tables' RLS, add
with (security_invoker = on). For views only used by admin or server
code, revoke select from anon and authenticated.
3. Revoke select on materialized views from public, anon, authenticated.
4. List security definer functions in public. Revoke execute from anon,
authenticated and public unless the function is meant to be public.
5. Show me the SQL before applying it, and don't fix any resulting
empty pages by adding using (true) policies.Then run check 1 and check 2 again, and open Security Advisor to confirm the errors are gone.
What an outside scan can and can't tell you
From outside, a scanner only sees views your site's code actually queries. It can confirm whether they return rows to your public key, but it can't tell whether that's intended (a public product list) or a leak (every customer's orders). The schema view is the reliable one: the policy checker sees every view and its settings, but not what the rows contain. Use both, then test as two different users.
Keep it that way
Views tend to appear when a feature needs a join, which is exactly when nobody is thinking about RLS. Abbi Labs scans your live app for leaked keys, Supabase tables and views readable with your public key, exposed .env/.git files and security headers. Monitor re-scans weekly and after every deploy and emails you only when something new appears.
FAQ
What does the Supabase "Security Definer View" warning mean?
It's the Security Advisor check 0010 ("View bypasses row-level security", level ERROR). Supabase's docs explain that Postgres views default to running with the permissions of the view's creator, not the person querying it. Views created in the dashboard or with the CLI are usually owned by a role that bypasses RLS, so a view in the public schema can return rows that your table policies would block.
How do I fix it?
If the view should respect RLS, set security_invoker on it: alter view public.my_view set (security_invoker = true); (Postgres 15 or later). Then the RLS policies and grants of the underlying tables apply to whoever queries the view. If the view should not be reachable from the API at all, revoke select on it from anon and authenticated, or move it out of the public schema.
Will switching to security_invoker break my app?
It can, if the view was quietly doing the work your policies don't. After the switch, signed-out and signed-in users only see the rows the underlying tables' policies allow, and they need SELECT grants on those tables. Test the pages that use the view as a signed-out visitor and as two different users before you ship.
What about materialized views?
RLS never applies to materialized views, and they can't use security_invoker. Supabase's advisor (check 0016) recommends revoking select from anon and authenticated, and the public role too, if they are reachable from the API.
Does Abbi Labs check views?
Partly, from two sides. The free Supabase RLS policy checker reads your schema (one read-only SQL query, analysed in your browser) and flags views and materialized views that API roles can read without security_invoker. The scan tests names your site's JavaScript queries through the REST API, which includes views, with your public key, one row max, and reports any that return data. It can't know what a view is supposed to show, so a readable view isn't always a bug.
Related guides
- How to check if your Supabase tables are readable by anyone
- Free tool: Supabase RLS policy checker
- Supabase "42501 permission denied for table": fix it with a GRANT, not by turning off RLS
- Is your Supabase storage bucket public? How to check it and lock it down
- Supabase service_role key exposed? Do this in the next 15 minutes
- Lovable app security checklist (from a security engineer)