RowShield
The RLS Field Guide

Appendix D · Migration-review checklist for AI-generated schema changes

The RLS Field Guide · 3 min read

Generators and assistants produce complete-looking migrations, and completeness of appearance is exactly the hazard: nothing in a plausible migration announces that its security half is missing. Review every schema-change pull request against this list. "No" is not automatically a rejection — but it must come with a written sentence from the author.

Tables

  • [ ] Every create table is followed in the same migration by enable row level security.
  • [ ] Grants follow RLS in the same file: explicit revoke all ... from anon, authenticated, then grants matching actual API usage.
  • [ ] Every table that holds tenant-scoped data carries the tenant id or an unambiguous path to it.
  • [ ] Any table deliberately left without policies (internal, locked-by-design) says so in a comment.

Policies

  • [ ] Every command the client performs (select / insert / update / delete) has its own named policy; no bare for all without justification.
  • [ ] Every policy names its role (to authenticated or similar); none rely on the default public.
  • [ ] No condition is constant true, and no insert/update policy ships with a null with check.
  • [ ] Multiple permissive policies on one table + command are listed with their derived union, in words.
  • [ ] Any rule that must constrain future policies uses as restrictive.

Destructive and reconstructive change

  • [ ] Every drop table accounts for the policies, grants, indexes, foreign keys, and publications attached to it.
  • [ ] Every drop ... cascade lists what the cascade removed (it reports each one as a NOTICE, not an error); a dropped foreign key is a silent loss of integrity, not a formality.
  • [ ] Column renames/rebuilds check for policies referencing the old name (they fail at evaluation time, not deploy time).
  • [ ] Anything resembling a drop-and-recreate of an existing table is treated as a security change, not just a schema change — Chapter 4, Pattern 2.
  • [ ] alter table ... disable row level security appears nowhere. If someone believes it is required, that belief is the review discussion.

Functions and views

  • [ ] Every security definer function has set search_path = '' (or an explicit safe path), fully qualified names inside, lives outside exposed schemas unless justified, and carries explicit execute grants.
  • [ ] New views set security_invoker = true (Postgres 15+) or document why owner-rights are intended.
  • [ ] Functions callable by clients state who may execute them (revoke ... from public present when needed).

Surfaces beyond tables

  • [ ] New storage buckets state their visibility and carry storage.objects policies for user-driven operations.
  • [ ] New realtime publication members have current policies, and delete-event visibility has been decided explicitly.
  • [ ] Edge-function changes touching the service key include the one-sentence justification for bypassing policies.

Tests

  • [ ] The change includes pgTAP coverage for new/changed policies: one allowed and one denied case per command per role touched (Chapter 6's shapes).
  • [ ] Existing isolation proofs still pass locally (supabase test db) before requesting review.
  • [ ] If the change alters policy expressions for performance reasons (Chapter 9), before/after plans are pasted into the description.

Process

  • [ ] The reviewer checks this list against the migration file itself, not the PR description.
  • [ ] Anything unexplained fails the review with a request for one specific sentence — never a silent approval.