RowShield
The RLS Field Guide

10 · Keeping it true

The RLS Field Guide · 4 min read

The question this chapter answers: how does all of this stay true six months and forty migrations from now?

Everything before this chapter produced a snapshot: correct policies, proofs that pass, an audit with clean results. Snapshots decay. The last discipline is structural — arrangements that make drift visible or impossible regardless of who is on the team, rather than relying on anyone's vigilance. Four mechanisms compose the loop.

1 · Migration review rules

Every schema change gets reviewed against a fixed set of questions (the full checklist lives in Appendix D). The core ones:

  • Every create table pairs, in the same migration, with enable row level security, grants, and policies.
  • Every drop table, column rename, or rebuild accounts for the policies attached to what it destroys.
  • New policy conditions state their rule in words in a comment; constant-true conditions require written justification.
  • security definer, unpinned search paths, new buckets, new publications: each appears only with its three-sentence explanation from Chapter 8.

Review against a checklist instead of judgment because generators produce plausible migrations and fatigue produces approvals. The checklist does not get tired.

2 · CI gates

Three gates, cheapest first.

A structural gate — assert the catalog's shape on every pull request. pgTAP again (testing docs):

-- supabase/tests/structure.test.sql
begin;
select plan(2);

select is_empty(
  $$select c.relname
    from pg_class c
    join pg_namespace n on n.oid = c.relnamespace
    where n.nspname = 'public' and c.relkind = 'r'
      and not c.relrowsecurity$$,
  'no public table has RLS disabled');

select is_empty(
  $$select tablename
    from pg_policies
    where schemaname = 'public'
      and permissive = 'PERMISSIVE'
      and (qual = 'true' or with_check = 'true')$$,
  'no tautological permissive policies');

select * from finish();
rollback;

The behavioral gate — Chapter 6's proof suite runs right behind it. Structural checks verify the configuration exists; proofs verify it behaves.

A text gate — cheap greps over the migrations directory catch vocabulary that should never merge silently:

grep -rniE "disable row level security" supabase/migrations/ && exit 1
grep -rniE "using\s*\(\s*true\s*\)" supabase/migrations/ && exit 1
grep -rn "security definer" supabase/migrations/ | grep -v "search_path" && exit 1

Each hit demands a human sentence beside it in review, not necessarily a rejection — but never silence.

3 · Scheduled re-audit

Run Chapter 5's afternoon audit after significant schema work, before launches, and on a calendar cadence you will actually keep — monthly for most teams. Two habits double its value: diff the findings against the previous run (a finding that disappeared deserves as much attention as one that appeared), and rotate who performs it. Fresh eyes find the assumptions the original author no longer sees.

4 · Monitoring

Between audits, something should notice change. A monitor worth reading reports transitions, not states: a finding created, a previously fixed finding regressed, a finding resolved. State-only reporting ("here are today's 14 warnings") trains everyone to ignore it by March; transition-based reporting pages once when something moves. RowShield's own alerting follows exactly this principle — created, regressed, resolved — for precisely this reason.

You can approximate it manually: keep audit.sql output under version control, and let any diff between runs become a ticket. What you cannot approximate manually is continuity — the monitor watches the days nobody chose to look, which is where Pattern 1 and Pattern 2 live.

When a proof fails

Have the playbook written before the red build:

  1. Freeze the surface. If reads leaked, revoke client grants on the affected table until understood; a loud outage beats a silent leak.
  2. Diff, don't debug. Compare pg_policies and grants against the last known-good run (audit file, CI artifact). Drift names itself in the diff.
  3. Fix forward in one reviewed migration, pairing every enable/revoke/policy statement with its neighbors so the fix cannot half-apply.
  4. Add the regression proof. Whatever slipped past must fail a named test from now on.
  5. Postmortem without blame. The question is never "who ran the migration" but "which mechanism should have caught it, and why didn't it exist yet."

The loop, assembled

Migrate → review against Appendix D → CI runs structure, proofs, greps → deploy → audit monthly → monitor transitions continuously → incidents feed new proofs and checklist items. None of the steps is heroic; all of them are schedulable. That is the point. Security that depends on someone remembering is drift waiting for a calendar gap; security that lives in migrations, tests, and diffs survives team changes, busy quarters, and the next generated schema.

The appendices compress everything repeatable into four reference pages: the accumulated audit checklist, the five proofs on one page, TaskHarbor's complete policy set, and the migration-review checklist. Keep them next to your terminal.

Check on your project

  1. Add the two structural assertions to your test suite and wire the three CI gates in order. Time the whole task; if it exceeds an hour, start with the structural gate alone.
  2. Put audit.sql under version control today, dated. Next month's diff is your first drift report.
  3. Choose your re-audit trigger and put it on the calendar owned by a role, not a person.
  4. Write the five-step playbook above into your incident process verbatim, before you need it.
  5. Adopt Appendix D as a required section in the template for schema-change pull requests.