Hey everyone,
Whenever I build multi-tenant setups on Supabase, relying on app-level middleware or tossing WHERE org_id =x inside Next.js Server Actions always feels risky. It works fine until someone forgets a filter on a late-night commit and leaks tenant data in prod.
I pushed isolation directly down to Postgresusing custom JWT claims in RLS, then wrote an integration test runner to try and break it.
Here’s the baseline setup:
Instead of running a subquery against org_memberships on every single row query, we pass org_idinside the Supabase session JWT claims.
The policy pattern for workspace tables:
SQL
create policy "Enforce organization isolation"
on public.workspace_data
for all
using (
org_id = (auth.jwt() -> 'app_metadata' ->> 'org_id')::uuid
)
with check (
org_id = (auth.jwt() -> 'app_metadata' ->> 'org_id')::uuid
);
Even if a ServerAction or API route completely omits a tenant check, Postgres blocks both reads and writes at the DB layer.
To make sure this actually holds up under weird edge cases, I set up integration tests that run before deploys to simulate bad actors:
The user discusses implementing and testing a robust RLS pattern for multi-tenant isolation in Supabase. They describe using custom JWT claims to enforce tenant isolation at the database level and detail eight test vectors to ensure security. The user seeks feedback on testing RLS policies and shares a resource for their setup.
Does it test the Security Definer functions for leakage?
Adding to the security definer point above, views are worth a test too. A Postgres view runs with the owner’s permissions by default, so a view over a tenant table can skip RLS entirely unless it’s created with security_invoker = true (Postgres 15+). One test that queries every view as a user from a different org would catch it.
I packaged this RLS pattern, the test runner, and a Next 16 / React 19 starter code athttps://andrady.coif anyone wants to check out the specs or test setup.
Curious how others here test their RLS policies before shipping to prod? Do you stick to JWT claims or do DB subqueries inside your policies?