RowShield
The RLS Field Guide

Appendix B · The five proofs on one page

The RLS Field Guide · 1 min read

TaskHarbor identities: ada c3c3c3c3-c3c3-c3c3-c3c3-c3c3c3c3c3c3 (Aster owner) · bo d4d4d4d4-d4d4-d4d4-d4d4-d4d4d4d4d4d4 (Borealis owner) · eve e5e5e5e5-e5e5-e5e5-e5e5-e5e5e5e5e5e5 (Aster member). Aster workspace a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1, its project …a1a2; Borealis project b2b2b2b2-b2b2-b2b2-b2b2-b2b2b2b2b2b3. Full CI-ready versions: Chapter 6.

P1 · Read isolation — bo must see his own task and none of Aster's:

begin;
set local role authenticated;
set local request.jwt.claim.sub = 'd4d4d4d4-d4d4-d4d4-d4d4-d4d4d4d4d4d4';
select count(*) as visible,
       count(*) filter (where project_id = 'a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a2') as aster
from public.tasks;                       -- expected: visible = 1, aster = 0
rollback;

P2 · Insert fence — eve cannot write into Borealis (must raise 42501):

begin;
set local role authenticated;
set local request.jwt.claim.sub = 'e5e5e5e5-e5e5-e5e5-e5e5-e5e5e5e5e5e5';
insert into public.tasks (project_id, title)
values ('b2b2b2b2-b2b2-b2b2-b2b2-b2b2b2b2b2b3', 'smuggled');  -- expected: ERROR 42501
rollback;

P3 · Update immobility — bo silently changes nothing in Aster (using filters first):

begin;
set local role authenticated;
set local request.jwt.claim.sub = 'd4d4d4d4-d4d4-d4d4-d4d4-d4d4d4d4d4d4';
update public.tasks set title = 'hijacked'
where project_id = 'a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a2'
returning id;                            -- expected: zero rows, no error
rollback;

P4 · Delete fence — bo deletes nothing in Aster:

begin;
set local role authenticated;
set local request.jwt.claim.sub = 'd4d4d4d4-d4d4-d4d4-d4d4-d4d4d4d4d4d4';
delete from public.tasks
where project_id = 'a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a2'
returning 1;                             -- expected: zero rows, no error
rollback;

P5 · Revocation — removing a member closes access immediately (was 2, now 0):

begin;
delete from public.members
 where workspace_id = 'a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1'
   and user_id      = 'e5e5e5e5-e5e5-e5e5-e5e5-e5e5e5e5e5e5';
set local role authenticated;
set local request.jwt.claim.sub = 'e5e5e5e5-e5e5-e5e5-e5e5-e5e5e5e5e5e5';
select count(*) from public.tasks;       -- expected: 0
rollback;

Adaptation rule: keep the three-part shape — identity (set local role + claim), one statement, one expected outcome — and assert counts for filtered operations, throws_ok-style errors for rejected writes. Seed known data first so a zero always means "filtered," never "empty table."