Multi-tenancy
Tenants are optional. They give the users of one project workspaces, teams or organizations with roles, invitations and tenant-scoped tables: the B2B “Acme’s workspace” model. They are unrelated to PotaLab Base organizations, which control who manages the project in the dashboard.
Until you enable tenants, the tenant endpoints answer 404 tenants_disabled, tokens carry no tenant claims and session.tenant is absent.
Enable
Section titled “Enable”In the dashboard open Auth → Tenants and choose Enable tenants. Scripts can use the Management API (POST /v1/projects/{ref}/auth/tenants/enable, scope auth:admin).
Enabling creates tables in the auth schema of your database (not accessible to anon, authenticated or service_role; everything goes through the APIs) and the policy helpers auth.tenant_id(), auth.tenant_role() and auth.has_tenant_role(text), which read only the token claims.
Disabling is refused (409 tenants_in_use) while policies, foreign keys or defaults use these objects; with a typed confirmation Base drops them and then every tenant, membership and invitation.
Active tenant
Section titled “Active tenant”Every access token carries the active tenant of its session:
{ "sub": "...", "role": "authenticated", "tenant_id": "6b0f...", "tenant_role": "admin" }A new session starts in the tenant the user used last (or the only one). auth.tenants.switch(id) makes another tenant active, only if the user is a member (403 not_tenant_member), and rotates the refresh token.
Membership is checked whenever a token is issued. A removed member keeps the tenant in the token they already hold until it expires or refreshes (at most the access-token lifetime). Turn on Strict membership to also sign out sessions that had the tenant active.
owner > admin > member. Any other name matching ^[a-z][a-z0-9_]{0,31}$ is a custom role that satisfies only itself (and member); owner satisfies every role.
| Action | Needs |
|---|---|
| read the tenant, list members, leave | member |
| rename, invite, change roles, remove members | admin (granting or removing owner needs owner) |
| delete the tenant | owner |
| create a tenant | any user when Users can create tenants is on (default), else only a server with a secret key |
A tenant always keeps at least one owner (409 last_owner).
const { data } = await base.auth.tenants.list() // { tenants: [...with role], active_tenant_id }const { data: acme } = await base.auth.tenants.create({ name: "Acme" }) // you become ownerawait base.auth.tenants.switch(acme!.id) // session.tenant = { id, role: "owner" }await base.auth.tenants.invite(acme!.id, { email: "ana@acme.com", role: "admin", redirectTo: "https://app.example.com/join" })
// at /join?type=tenant_invite&token=... once Ana is signed in with ana@acme.comawait base.auth.tenants.acceptInvite(token)
await base.auth.tenants.members(acme!.id) // updateMember, removeMember, leave, get, update, deleteawait base.auth.tenants.invitations(acme!.id) // revokeInvitationInvitations are emailed through the project’s mail settings (customizable with the tenant_invite email template), are single use and expire after 7 days. Accepting requires the signed-in user’s email to match the invited one.
Policies
Section titled “Policies”Use the helpers in the (select ...) form so Postgres evaluates them once per statement. Always add a restrictive guard: permissive policies are OR-ed, so any broader permissive policy added later could otherwise let rows cross tenants.
create table public.projects ( id bigint generated by default as identity primary key, tenant_id uuid not null default auth.tenant_id() references auth.tenants (id) on delete cascade, name text not null);create index on public.projects (tenant_id);alter table public.projects enable row level security;
create policy projects_tenant_guard on public.projects as restrictive for all to anon, authenticated using (tenant_id = (select auth.tenant_id())) with check (tenant_id = (select auth.tenant_id()));
create policy projects_select on public.projects for select to authenticated using (tenant_id = (select auth.tenant_id()));create policy projects_insert on public.projects for insert to authenticated with check (tenant_id = (select auth.tenant_id()));create policy projects_delete on public.projects for delete to authenticated using (tenant_id = (select auth.tenant_id()) and (select auth.has_tenant_role('admin')));grant select, insert, update, delete on public.projects to authenticated;The dashboard table editor offers Tenant scoped and Tenant scoped + owner rows presets that create the column, index, guard and policies for you. For storage use a folder per tenant and (storage.foldername(path))[1] = (select auth.tenant_id())::text.
Settings
Section titled “Settings”| Setting | Default | Effect |
|---|---|---|
allow_user_create |
true |
end users may create tenants (up to 50 each) |
signup_requires_invitation |
false |
email sign-up only for addresses with a pending tenant invitation |
strict_membership |
false |
sign out sessions when a member is removed |
domain_mappings |
[] |
users with a verified email on a domain (or an SSO provider’s domains) join a tenant at sign-in |