Supahound is in early access. Sign up now; features marked Coming soon switch on as they ship. See what's included

Supahound

Postgres best practices

Multi-tenant RLS in Supabase: keep each team's data apart

Give every tenant-owned table a `tenant_id`, keep a `memberships` table that maps users to tenants, and write every policy as "the row's tenant is one of my tenants". Then test it: sign in as a user from tenant A and prove that every query for tenant B returns nothing.

Last updated

What does the schema look like?

create table public.tenants (
  id uuid primary key default gen_random_uuid(),
  name text not null
);

create table public.memberships (
  tenant_id uuid not null references public.tenants (id) on delete cascade,
  user_id uuid not null references auth.users (id) on delete cascade,
  role text not null check (role in ('owner', 'member')),
  primary key (tenant_id, user_id)
);
create index memberships_user_idx on public.memberships (user_id);

create table public.projects (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants (id) on delete cascade,
  name text not null
);
create index projects_tenant_idx on public.projects (tenant_id);

How do I write the tenant policy?

The `with check` matters: without it, a user could insert a row into a tenant they do not belong to.

alter table public.projects enable row level security;

create policy "projects: members read" on public.projects
  for select to authenticated
  using (tenant_id in (
    select tenant_id from public.memberships where user_id = (select auth.uid())
  ));

create policy "projects: members write" on public.projects
  for insert to authenticated
  with check (tenant_id in (
    select tenant_id from public.memberships where user_id = (select auth.uid())
  ));

Where do cross-tenant leaks come from?

  • A new table added without `tenant_id` or without a policy.
  • A view that runs as its owner and so skips RLS (see the security definer guide).
  • An RPC function marked `security definer` that takes a tenant ID as a parameter and never checks membership.
  • Tenant IDs read from `user_metadata`, which the user can change.

How do I prove tenants are isolated?

Write a test that creates two tenants with one user each, signs in as user A and tries to select, update and delete tenant B's rows through the API. Every attempt must return zero rows. Run it in CI, and add every new table to it. A test that lists tables from `pg_class` and fails when a table lacks `tenant_id` or RLS catches the most common regression.

Frequently asked questions

Should I put the tenant ID in the JWT?
You can put it in `app_metadata` through a custom access token hook, which saves a lookup. Never put it in `user_metadata`, because users can edit that.
One schema per tenant or tenant_id columns?
For most SaaS apps, a `tenant_id` column with RLS is simpler to migrate and query. Schema-per-tenant makes sense only for strong isolation needs and a small number of large tenants.
How does Supahound help with multi-tenant apps?
Supahound reads your RLS policies to learn who owns which rows, then watches the logs for one actor reading rows from many owners. That is the traffic pattern of a cross-tenant leak.

Find out if someone is abusing your app

Connect your Supabase org read-only. The free posture scan runs in minutes. No credit card.