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 tablepairs, in the same migration, withenable 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:
- Freeze the surface. If reads leaked, revoke client grants on the affected table until understood; a loud outage beats a silent leak.
- Diff, don't debug. Compare
pg_policiesand grants against the last known-good run (audit file, CI artifact). Drift names itself in the diff. - Fix forward in one reviewed migration, pairing every enable/revoke/policy statement with its neighbors so the fix cannot half-apply.
- Add the regression proof. Whatever slipped past must fail a named test from now on.
- 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
- 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.
- Put
audit.sqlunder version control today, dated. Next month's diff is your first drift report. - Choose your re-audit trigger and put it on the calendar owned by a role, not a person.
- Write the five-step playbook above into your incident process verbatim, before you need it.
- Adopt Appendix D as a required section in the template for schema-change pull requests.