Your Postgres view bypasses row-level security. Here's the fix.

Row-level security on a table doesn't follow it into a view by default. The view checks its owner's rights, not the caller's. A local test, the fix and an audit query.

The short answer

A PostgreSQL view checks the tables beneath it with the view owner’s permissions and applies the view owner’s row-level security policies, not the caller’s.1 Views are usually created by the role that owns the tables, and table owners bypass row security unless the table is forced.3 So a caller who sees one row through the table can see every row through the view.

The fix, on PostgreSQL 15 and later, is one option: ALTER VIEW my_view SET (security_invoker = true);. Then the caller’s own policies and permissions apply, as if they had queried the table directly.1

Deedglass reads what Postgres actually committed, not what the agent reported. In our demo a hidden trigger turned 1 row into 301; a person countersigned the undo and all 301 rows came back.

Why a view skips the policy

Before security_invoker, a view was a way to hand out restricted access: you grant SELECT on the view, not the table, and the view runs with its owner’s rights. That’s still the default. The CREATE VIEW docs say that if an underlying table has row-level security enabled, the view owner’s policies are applied by default.1

Who is the view owner? Usually whoever ran the migration, which is usually the table owner or an admin role. Supabase’s docs put it bluntly: views bypass RLS by default because they are usually created with the postgres user, and a view over a protected table hands out the rows its policies were meant to withhold.2

Nothing warns you. The query succeeds and returns more rows. (If row-level security itself is new to you, start with the glossary entry.)

A local test

We ran this on PostgreSQL 18.6, as a superuser creating everything (the way a migration usually does):

Code A database command in SQL. The text around it says what it does.

create role app_user nologin;
create table notes(id int primary key, owner text, body text);
insert into notes values (1,'alice','a1'),(2,'bob','b1'),(3,'bob','b2');
alter table notes enable row level security;
create policy own on notes using (owner = current_setting('app.uid', true));
grant select on notes to app_user;

create view notes_v as select * from notes;
create view notes_v_inv with (security_invoker = true) as select * from notes;
grant select on notes_v, notes_v_inv to app_user;

set app.uid = 'alice';
set role app_user;
select 'table as app_user' as q, count(*) from notes;
select 'view (default) as app_user' as q, count(*) from notes_v;
select 'view security_invoker as app_user' as q, count(*) from notes_v_inv;

What psql printed, trimmed to the rows:

Code What the screen or file shows, copied as is.

 table as app_user                 |     1
 view (default) as app_user        |     3
 view security_invoker as app_user |     1

Same role, same session setting. The table and the invoker view return Alice’s one row. The default view returns all three.

The fix

Set the option on existing views:

Code A database command in SQL. The text around it says what it does.

alter view notes_v set (security_invoker = true);

We ran that against the same database and queried again as app_user:

Code What the screen or file shows, copied as is.

 view after ALTER VIEW |     1

For new views, put it in the definition: create view ... with (security_invoker = true) as .... Supabase recommends the same option for Postgres 15 and above.2

Two things to know before flipping it everywhere:

  1. The caller now needs grants on the base tables. With security_invoker, base tables are checked against the user of the view.1 A view that was the only grant a role had will start returning permission errors. That’s usually what you want: it shows you which roles relied on the owner’s rights.
  2. Policies must exist for the caller’s role. If RLS is on and no policy matches the caller, they get nothing, because row security denies by default.

Find every view that skips RLS

This lists views in your own schemas without security_invoker set. We ran it on the test database before the fix:

Code A database command in SQL. The text around it says what it does.

select n.nspname as schema, c.relname as view,
       coalesce(c.reloptions::text, '{}') as options
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where c.relkind = 'v'
  and n.nspname not in ('pg_catalog','information_schema')
  and not coalesce('security_invoker=true' = any(c.reloptions)
                   or 'security_invoker=on' = any(c.reloptions), false);

Code What the screen or file shows, copied as is.

 schema |  view   | options
--------+---------+---------
 public | notes_v | {}

Not every hit is a bug. Some views exist precisely to give restricted access with the owner’s rights, for example a view that exposes only a few columns. Go through the list and sort each view into one of three groups:

View Over a table with RLS? Action
Meant to show only the caller’s rows Yes Set security_invoker = true
Meant to expose a safe subset to everyone (aggregate, public columns) Either Keep owner rights, document why, check it hides what you think
Over tables without RLS No Low risk from this issue; check the table’s grants instead

Why this matters more with agents

An AI agent with database access explores. Ask it to “find Bob’s notes” and it will list the schema, see notes and notes_v, and query whichever looks useful. A human developer tends to know which view is the reporting view. An agent doesn’t, and it will happily read the one that returns more.

It also writes. We granted UPDATE on a third default view, notes_w, and ran this as app_user with app.uid still set to Alice:

Code A database command in SQL. The text around it says what it does.

update notes_w set body = 'changed' where owner = 'bob';

Code What the screen or file shows, copied as is.

UPDATE 2

Both of Bob’s rows changed, through a role whose policy shows it only Alice’s row. If your agent’s database role can update views, run the audit query above first. And if the agent connects with an admin key, views are the least of it: see the Supabase service_role key for what a BYPASSRLS role reaches.

What security_invoker doesn’t fix

  • Functions. A function declared SECURITY DEFINER is another place where code runs as its owner rather than its caller. List them and review each one separately.
  • Owners and bypass roles. Superusers and roles with BYPASSRLS always bypass row security, and so does a table owner unless the table has FORCE ROW LEVEL SECURITY.3 If the agent’s role owns the tables, no view setting helps.
  • Silent filtering. RLS hides rows without an error, which can make an agent think data is missing. For backups and checks, row_security = off turns filtering into an error instead of bypassing it.3

For the wider setup (a narrow role, policies per customer, statement limits), read safe Postgres access for AI agents. To see what an agent configured on your machine could reach, paste the agent’s MCP config into the check page or run npx deedglass scan (how the grade works). To try the checks in your browser without installing anything, open the playground.

Questions people ask

Does row-level security apply to views in Postgres?

Not the caller’s policies, by default. The view owner’s policies and permissions apply unless the view has security_invoker = true.1

Which Postgres version supports security_invoker?

PostgreSQL 15 and later. Supabase’s docs give the same version for the fix.2

Will setting security_invoker break my app?

It can, if a role had grants only on the view. With the option on, the caller needs its own grants and matching policies on the base tables.1

Written with AI assistance and checked against our claims register.

The takeaway

A plain view returned 3 of 3 rows to a role whose policy allowed 1; with security_invoker = true it returned 1. Audit every view before an agent can query it.

Read nextThe Supabase service_role key and AI agents: why it skips every policy Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1PostgreSQL docs: CREATE VIEW · postgresql.org · checked 26 October 2026
  2. 2Supabase docs: Row Level Security · supabase.com · checked 26 October 2026
  3. 3PostgreSQL docs: Row Security Policies · postgresql.org · checked 26 October 2026

Published 26 October 2026, sources last checked 26 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing