Test what a signed-in user must not be allowed to do.
A Supabase policy is tenant-safe only when it relates the current identity to the tenant that owns the target row. Checking auth.uid() somewhere in a query is not enough if that relationship is missing.
Create two tenants and two users
Give user A membership only in tenant A and user B membership only in tenant B. Seed one protected row per tenant, then run every operation as both users.
- Confirm each user can perform the intended operation inside their own tenant
- Replace the row or tenant identifier with the other tenant's value
- Expect zero rows, a policy violation, or an application-safe not-found response
- Repeat with changed JWT metadata and direct API requests
Correlate membership to the row being accessed
The membership check must compare against the protected row's tenant key. A policy that merely proves the user belongs to any organization can authorize every row.
sqlcreate policy "members read projects"
on public.projects for select
to authenticated
using (
exists (
select 1
from public.organization_memberships m
where m.organization_id = projects.organization_id
and m.user_id = auth.uid()
)
);Test USING and WITH CHECK separately
USING controls which existing rows an UPDATE or DELETE can target. WITH CHECK controls whether the proposed INSERT or updated row is allowed. Testing only reads leaves tenant-key reassignment and cross-tenant inserts uncovered.
Check the migration before runtime testing
BoundaryCI catches deterministic final-state mistakes before a disposable Supabase environment is created. Runtime tests remain valuable for claims, joins, RPC, storage, and application behavior that static analysis cannot prove.
bashnpx boundaryci scan supabase --profile supabase --fail-on high