With the Data API change (discussion #45329, applied to all projects on 2026-10-30), grants become explicit. Two mismatches between grants and RLS policies then become worth surfacing:
privilege_without_policy — RLS is enabled, anon/authenticated hold a table privilege (SELECT/INSERT/UPDATE/DELETE, or TRUNCATE which RLS doesn't cover), but no permissive policy allows that command for that role. RLS still blocks the rows, so nothing leaks today; the grant is just wider than the policies need. Proposed level: INFO (same reasoning as 0008).
policy_without_privilege — a permissive policy exists for a role/command, but the role has no table (or column) privilege. The policy is dead code and requests fail with 42501 before it is evaluated. Expected to become common after Oct 30. Proposed level: INFO.
How this differs from 0026/0027: those flag relations a role can read (introspection exposure). These compare the grant set against the policy set per role and command, in both directions.
Data: a static replay of migrations across 580 unique public Lovable+Supabase repos (write-up: https://github.com/larik15/supabase-grants/blob/main/STUDY.md). Between 17% and 78% of them rely on the legacy default grants, so on older projects lint 1 would fire on many RLS tables. That's why I'd keep it INFO, one row per table+role, and possibly limit it to write privileges.
Questions before I write code:
Implementation notes: has_table_privilege / has_any_column_privilege (covers PUBLIC and role membership), polroles containing the role or {public}, polcmd '*' expansion, relkind 'r', the same schema exclusion list as 0013, pg_depend deptype 'e' exclusion, pgrst.db_schemas scope. Happy to follow the /new-lint skill.
Larik proposes two INFO level lints to address mismatches between grants and RLS policies due to upcoming Data API changes. These lints aim to identify when privileges exist without corresponding policies and vice versa, potentially leading to dead code or unexpected behavior. Shubham sahu supports the proposal, suggesting additional considerations for implementation.
Both look useful to me, and lint 2 in particular is going to catch real 42501s after Oct 30. From running RLS in an app, here are three cases I'd fold into the design before writing code:
1. Commands that also need SELECT. Per Postgres's "policies applied by command type" table, an UPDATE or DELETE that reads the table (any WHERE, which is every .update().eq() / .delete().eq() from supabase-js) also applies the SELECT policies to the existing rows. INSERT ... RETURNING (.insert().select()) applies them to the new row, and an upsert needs INSERT, UPDATE and SELECT. So "UPDATE policy + UPDATE grant, but no SELECT policy for that role" isn't dead code in either of your two senses, yet it's probably the most common RLS support question: the update succeeds with 0 rows and no error, and the insert-then-select fails with "new row violates row-level security policy". The same thing happens on the grant side (UPDATE with a WHERE needs SELECT privilege on the columns it reads). The per-role/per-command matrix you're already building answers it for free, so maybe a third rule, or a note in lint 2's remediation text.
2. relkind. 'r' alone misses partitioned tables ('p'). When you query through the parent, only the parent's policies and grants apply, not the partitions'. So I'd check 'p' and skip relispartition children, or they'll show up as noise.
3. Legacy-grant noise on lint 1. Given your 17-78% numbers, limiting it to write privileges sounds right. I'd give TRUNCATE its own line instead of mixing it into the SELECT/INSERT/UPDATE/DELETE rows, because no policy can ever narrow it: the grant is the only gate. PostgREST doesn't expose TRUNCATE, so today it's latent, but the remediation is different ("revoke", not "add a policy or narrow the grant").
On sequencing, lint 2 alone as a first PR makes sense to me. It should be low on false positives, and it's the one people will hit right after the change.