Fix guide · tested 18 October 2026
How to fix a security definer view in Supabase
Supabase's Security Advisor reports "Security Definer View" (lint 0010, level ERROR) when a view in the public schema runs with its creator's privileges and so ignores Row Level Security.
Supabase flags a security definer view because, by default, a Postgres view runs with the privileges of the role that created it, not the user querying it. Views created in Supabase are usually owned by a role that bypasses RLS, so the view can return every row of tables you've protected. Fix it by setting security_invoker on the view: alter view public.your_view set (security_invoker = on);. The view then respects the caller's RLS policies. Find affected views first with the read-only query on this page, and test before and after.
How we tested this: We ran every SQL statement on this page on PostgreSQL 17.11 with Supabase-style anon and authenticated roles and a stand-in for auth.uid(), not on a live Supabase project, on 18 October 2026. With the default setting, a signed-in user querying the view saw both users' rows despite an owner-only RLS policy on the table; after setting security_invoker, they saw only their own row.
Why it matters
Supabase's lint documentation explains that Postgres defaults views to running with the view creator's permissions, for backwards compatibility. In Supabase, views made in the SQL editor or the CLI are typically owned by postgres, which can bypass RLS. Put that view in the public schema, which the API exposes, and anyone allowed to select from the view sees every row of the underlying tables, whatever your RLS policies say.
This is easy to miss. You can secure a table properly, then add a convenience view for a dashboard or a join, and quietly reopen the table.
Check it first (read-only)
Run this in the Supabase SQL editor. It only reads the system catalog. Each row is a view in public that doesn't have security_invoker set.
select c.relname as view_name
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 o ~* '^security_invoker=(on|true|yes|1)$'
)
order by 1;The Security Advisor in the Supabase dashboard runs the same lint. Materialized views can't use security_invoker; if you have one in public, move it to a schema the API doesn't expose.
The fix, step by step
- List the views. Run the read-only query. For each view it returns, check which tables it reads and whether those tables have RLS policies you rely on.
- Switch the view to security_invoker. Set the option on the existing view. You don't need to drop and recreate it. Postgres supports
security_invokeron views from version 15; check your project's Postgres version under Settings if you're unsure.alter view public.profile_emails set (security_invoker = on); - Or set it when you create the view. For new views, and in your migrations, include the option in the definition so it can't be forgotten.
create view public.profile_emails with (security_invoker = on) as select id, email from public.profiles; - Check grants on the view. The view's own grants still apply. If signed-out visitors shouldn't use it, revoke select from anon.
revoke select on public.profile_emails from anon; - Test as two users. Query the view as user A and confirm you see only rows the table's policies allow. Re-run the read-only query; it should return no rows.
When a definer view is intentional
Sometimes you want a view to show more than the caller could read directly, for example aggregate counts across all users. Don't leave that in the public schema. Move it to a schema the API doesn't expose and read it from a server, or replace it with a function that returns only the aggregate and check that function's EXECUTE grants.
Common mistakes
- Fixing the table but not the view. Re-run the view check after any RLS work. See fix RLS disabled in public for the tables.
- Recreating the view without the option. A later
create or replace viewin a migration can drop the setting. Putwith (security_invoker = on)in the migration itself. - Assuming the API hides views. Views in the public schema are exposed through the API just like tables.
FAQ
What is a security definer view?
A view that runs with the privileges of the role that owns it rather than the user querying it. In Postgres that's the default for views, so a view owned by a role that bypasses RLS ignores your RLS policies.
How do I fix a security definer view in Supabase?
Run alter view public.your_view set (security_invoker = on); so the view runs with the caller's privileges and respects RLS. Then test as two users.
Will security_invoker break my view?
It can, if the view relied on reading rows the caller can't see. After the change, users only see rows the underlying tables' policies allow, which is usually what you want.
Can I check this without changing anything?
Yes. The catalog query on this page only reads system tables, and the Security Advisor in the Supabase dashboard runs the same lint.
Do materialized views support security_invoker?
No. Keep materialized views out of the public schema, or out of any schema the API exposes.
Want someone to check it for you?
We check this as part of the $49 AI Site Audit. See how we test and what we don't check.
How this guide was made: researched and drafted with AI assistance, then every step was run and checked as described above on 18 October 2026. Editorial responsibility: ChikuOS. Found a problem? Email hello@chikuos.com.