Summary
Add a small as_anonymous(db) context manager (or, if simpler, document the recipe pattern) for testing RLS behavior as the anon Postgres role with no JWT claims. The existing as_rls_user(db, user_id, role="anon") mostly works but sets a sub claim that anon users shouldn't have, which can hide bugs where a policy implicitly trusts the presence of sub.
Motivation
Several RLS bug classes only surface when the requester is genuinely anonymous:
- Anon falling through a policy because
auth.uid() returns NULL and the policy expression doesn't handle NULLs explicitly (auth.uid() = user_id evaluates to NULL → false silently, returning an empty array; common cause of "the API works but returns nothing").
- Tables where the developer forgot to enable RLS, so
anon's default SELECT grant on public.* exposes the table.
- Policies whose USING clause implicitly trusts
request.jwt.claims->>'sub' being present (rather than handling it being NULL).
Today the test author writes something like:
db.execute("SET LOCAL ROLE anon")
try:
rows = db.query("SELECT * FROM tickets")
finally:
db.execute("RESET ROLE")
That works but isn't discoverable, is easy to forget the RESET, and doesn't compose with the existing as_rls_user style.
Proposed API
from sqlproof.contrib.supabase import as_anonymous
with as_anonymous(db):
rows = db.query("SELECT * FROM tickets")
assert rows == [] # if RLS is correctly enforcing
Implementation: ~15 lines, mirrors as_rls_user's structure (savepoint-friendly, exception-safe, RESET ROLE on exit). Crucially, it does not call set_config('request.jwt.claims', ...) — so auth.uid() truly returns NULL inside the block, modeling real anonymous requests rather than authenticated-as-anon.
Alternative: docs-only
If a new helper feels like API-surface bloat, document the raw SET LOCAL ROLE pattern in guides/supabase.md with an explicit callout about "why you can't just pass role='anon' to as_rls_user".
Related
- Inbox sample design spec — see "Gap 2" in the RLS bug-classes analysis:
docs/superpowers/specs/2026-06-03-inbox-sample-design.md
src/sqlproof/contrib/supabase.py — existing as_supabase_user, as_rls_user
Summary
Add a small
as_anonymous(db)context manager (or, if simpler, document the recipe pattern) for testing RLS behavior as theanonPostgres role with no JWT claims. The existingas_rls_user(db, user_id, role="anon")mostly works but sets asubclaim that anon users shouldn't have, which can hide bugs where a policy implicitly trusts the presence ofsub.Motivation
Several RLS bug classes only surface when the requester is genuinely anonymous:
auth.uid()returns NULL and the policy expression doesn't handle NULLs explicitly (auth.uid() = user_idevaluates to NULL → false silently, returning an empty array; common cause of "the API works but returns nothing").anon's defaultSELECTgrant onpublic.*exposes the table.request.jwt.claims->>'sub'being present (rather than handling it being NULL).Today the test author writes something like:
That works but isn't discoverable, is easy to forget the
RESET, and doesn't compose with the existingas_rls_userstyle.Proposed API
Implementation: ~15 lines, mirrors
as_rls_user's structure (savepoint-friendly, exception-safe,RESET ROLEon exit). Crucially, it does not callset_config('request.jwt.claims', ...)— soauth.uid()truly returns NULL inside the block, modeling real anonymous requests rather than authenticated-as-anon.Alternative: docs-only
If a new helper feels like API-surface bloat, document the raw
SET LOCAL ROLEpattern inguides/supabase.mdwith an explicit callout about "why you can't just passrole='anon'toas_rls_user".Related
docs/superpowers/specs/2026-06-03-inbox-sample-design.mdsrc/sqlproof/contrib/supabase.py— existingas_supabase_user,as_rls_user