Organization
Organizations, members, invitations, teams, roles, and active-org sessions.
The Organization plugin adds multi-tenant organizations: membership, email invitations, an active organization on the session, optional teams, and optional dynamic access-control roles. It pairs with an access-control policy so members can be checked for permissions before they act.
Installation
Add the plugin to your server
Pass organization() to your Kernia config. Supply a send_invitation callback
to deliver invite emails; enable teams and dynamic_access_control if you need
them.
from kernia import KerniaOptions
from kernia.auth import init
from kernia.plugins.organization import organization
async def send_invitation(invitation: dict) -> None:
await email_client.send(
to=invitation["email"],
subject="You've been invited to Acme",
html=f'Accept: https://app.example.com/accept/{invitation["id"]}',
)
auth = init(KerniaOptions(
database=adapter,
secret=os.environ["KERNIA_SECRET"],
base_url=os.environ["KERNIA_BASE_URL"],
plugins=[
organization(
teams=True,
dynamic_access_control=True,
invitation_expires_in=60 * 60 * 24 * 2,
send_invitation=send_invitation,
),
],
))Add the client plugin
Add the organization client plugin:
import { createAuthClient } from "better-auth/client";
import { organizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: "/api/auth",
plugins: [organizationClient()],
});Usage
Create an organization
const { data: org } = await authClient.organization.create({
name: "Acme",
slug: "acme",
logo: "https://example.com/logo.png",
});The creator becomes the owner. Use checkSlug({ slug }) first if you need to
validate availability.
List and set the active organization
const { data: orgs } = await authClient.organization.list();
await authClient.organization.setActive({ organizationId: "org_123" });setActive writes activeOrganizationId onto the current session so subsequent
calls scope to that org.
Get full details
const { data } = await authClient.organization.getFullOrganization({
organizationId: "org_123",
});Returns the organization with its members, invitations, and teams.
Invite and accept members
await authClient.organization.inviteMember({
email: "user@example.com",
role: "member",
organizationId: "org_123",
});
await authClient.organization.acceptInvitation({ invitationId: "inv_123" });inviteMember creates a pending invitation and calls your send_invitation
callback.
Manage members
const { data } = await authClient.organization.listMembers({ organizationId: "org_123" });
await authClient.organization.updateMemberRole({ memberId: "mem_123", role: "admin" });
await authClient.organization.removeMember({ memberIdOrEmail: "user@example.com", organizationId: "org_123" });
await authClient.organization.leave({ organizationId: "org_123" });Teams
Available when the server is configured with teams=True.
await authClient.organization.createTeam({ name: "Platform", organizationId: "org_123" });
const { data: teams } = await authClient.organization.listTeams({ organizationId: "org_123" });Check permissions
const { data } = await authClient.organization.hasPermission({
permissions: { member: ["delete"] },
});
// Check a hypothetical role without an extra round-trip:
const allowed = authClient.organization.checkRolePermission({
role: "admin",
permissions: { member: ["delete"] },
});hasPermission checks the current member against the server-side policy;
checkRolePermission evaluates a role locally.
Options
| Option | Type | Default | Description |
|---|---|---|---|
teams | bool | False | Register team CRUD routes and add team / teamMember tables. |
dynamic_access_control | bool | False | Register role CRUD routes and add the organizationRole table. |
invitation_expires_in | int | 172800 (2 days) | Invitation lifetime in seconds. |
send_invitation | async (invitation) -> None | None | Called with the invitation row after it is created — deliver the email here. |
Schema
Core tables are always added; teams and dynamic access control add more when
enabled. The session table is extended with activeOrganizationId (and
activeTeamId when teams are on).
idnamesluglogometadatacreatedAtupdatedAtidorganizationIduserIdrolecreatedAtidorganizationIdemailrolestatusinviterIdteamIdexpiresAtcreatedAtWhen teams=True, the plugin also adds team and teamMember. When
dynamic_access_control=True, it adds organizationRole (role name +
JSON permissions per organization).
The active organization lives on the session via setActive. Routes that mutate
membership scope to it, so set an active organization before calling member or
team operations that omit an explicit organizationId.