Skip to content

Supabase security definer view: why it skips RLS

Supabase

View as MarkdownOpen in ClaudeOpen in ChatGPTOpen in Perplexity

The question is copied too. If the chat opens empty, paste it.

TL;DR

A Postgres view runs with the rights of its owner unless it is created with security_invoker = true. In Supabase the owner is usually postgres, which bypasses RLS, so a view over a protected table returns rows the table itself hides. On my local stack the public key got 0 rows from the table and all 3 of 3 from a plain view over it; with security_invoker = true the view returned 0 too. On Postgres 15 and later, set it on every view in an exposed schema.

My free Supabase security checker has a rule called “Views that bypass RLS”, and Supabase’s Security Advisor reports the same thing as security_definer_view. It is easy to miss, because the table it leaks from looks protected.

Why does a Supabase view bypass RLS?

A view is a stored query. By default, Postgres checks permissions and RLS on the tables inside it as the view’s owner, not as the person calling it. Supabase’s RLS guide puts it directly: views bypass RLS by default because they are usually created with the postgres user, and a view over a protected table hands out every row its policies were meant to withhold.

The Data API serves views like tables. If the view is in an exposed schema and anon or authenticated can select from it, the public key reads it.

What did the view return in my test?

On a local Supabase stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14) I created a table orders with three fake customers, enabled RLS with no policy, and a view order_summary over it with a select grant for anon. Then I called both with the publishable key:

Same table, three ways to read it. Local Supabase, RLS on with no policy, publishable key. The table itself: Publishable key → orders, RLS on → 0 rows; A plain view over it: Publishable key → order_summary view → 3 of 3 rows; The view with security_invoker: Publishable key → security_invoker view → 0 rows.
My reproduction on a local Supabase stack, 23 October 2026.
Request with the publishable key Result
GET /rest/v1/orders (RLS on, no policy) []
GET /rest/v1/order_summary (plain view) all 3 rows, with names and emails
GET /rest/v1/order_summary after security_invoker = true []

Same table, same key. Only the view’s setting changed what came back.

How do you fix it?

On Postgres 15 and later, which every current Supabase project runs, make the view check RLS as the caller:

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

For a new view, put it in the definition:

create view public.order_summary
with (security_invoker = true)
as select customer, email, total from public.orders;

The caller then also needs select on the underlying table, and the table’s policies decide the rows. If a view must aggregate data the caller cannot see row by row, such as a public count, keep it out of the exposed schema and serve it through a function with a narrow result instead.

To list the views in public that still run as their owner:

select c.relname as view
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'v'
  and not coalesce(
    (select bool_or(option = 'security_invoker=true' or option = 'security_invoker=on')
     from unnest(c.reloptions) as option), false);

On my stack, with one plain view and two with security_invoker = true, it returned only the plain one.

Materialized views do not support RLS at all, so the checker flags them in the API separately, under “Materialized views or foreign tables in the API”. What else goes around policies, from the secret key to SECURITY DEFINER functions, is in my Lovable app security checklist, and how to prove the policies themselves is in how to test Supabase RLS.

© 2026 Denys Zaichenko. Original article: https://madsalt.tech/notes/supabase-security-definer-view/