Mass assignment — UPDATE policies without WITH CHECK
Problem
Section titled “Problem”You ship a policy: “members can edit their own row in org_members.” A viewer issues UPDATE org_members SET role = 'admin' WHERE user_id = auth.uid() and silently becomes an admin.
The code
Section titled “The code”Why review misses it
Section titled “Why review misses it”USING (user_id = auth.uid()) reads as “members can only touch their own row” — and that’s true. The blind spot is between “which rows can they touch” (USING) and “what state can the row end up in” (WITH CHECK). Reviewers conflate the two.
The example test that passes
Section titled “The example test that passes”The test confirms members can update — but doesn’t check what they can change.
The SqlProof property
Section titled “The SqlProof property”The key idea: assert the post-state, not the return value. The UPDATE may raise (with the fix) or quietly apply (without). The only evidence of whether the bug is present is in the row.
Note: db.savepoint() is used around the UPDATE so that if the WITH CHECK raises a policy-violation error, the transaction stays open for the post-state read. Without it the aborted transaction would prevent the verification query from running.
The counterexample
Section titled “The counterexample”Illustrative — Hypothesis would print the actual draw and assertion:
The fix
Section titled “The fix”Add WITH CHECK that pins the new row’s role to 'viewer' — the new row must still be a viewer:
This is enum-stable: any future role added to the member_role enum is automatically denied without updating this policy. Role promotions must go through a SECURITY DEFINER admin function instead — that’s the standard Supabase pattern.
See also Missing DELETE policy — the sibling write-side RLS bug.