| name | organization-best-practices |
| description | This skill provides guidance and enforcement rules for implementing multi-tenant organizations, teams, and role-based access control using Better Auth's organization plugin. |
Setting Up Organizations
When adding organizations to your application, configure the organization plugin with appropriate limits and permissions.
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
allowUserToCreateOrganization: true,
organizationLimit: 5,
membershipLimit: 100,
}),
],
});
Note: After adding the plugin, run npx @better-auth/cli migrate to add the required database tables.
Client-Side Setup
Add the client plugin to access organization methods:
import { createAuthClient } from "better-auth/client";
import { organizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [organizationClient()],
});
Creating Organizations
Organizations are the top-level entity for grouping users. When created, the creator is automatically assigned the owner role.
const createOrg = async () => {
const { data, error } = await authClient.organization.create({
name: "My Company",
slug: "my-company",
logo: "https://example.com/logo.png",
metadata: { plan: "pro" },
});
};
Controlling Organization Creation
Restrict who can create organizations based on user attributes:
organization({
allowUserToCreateOrganization: async (user) => {
return user.emailVerified === true;
},
organizationLimit: async (user) => {
return user.plan === "premium" ? 20 : 3;
},
});
Creating Organizations on Behalf of Users
Administrators can create organizations for other users (server-side only):
await auth.api.createOrganization({
body: {
name: "Client Organization",
slug: "client-org",
userId: "user-id-who-will-be-owner",
},
});
Note: The userId parameter cannot be used alongside session headers.
Active Organizations
The active organization is stored in the session and scopes subsequent API calls. Always set an active organization after the user selects one.
const setActive = async (organizationId: string) => {
const { data, error } = await authClient.organization.setActive({
organizationId,
});
};
Many endpoints use the active organization when organizationId is not provided:
await authClient.organization.listMembers();
await authClient.organization.listInvitations();
await authClient.organization.inviteMember({ email: "user@example.com", role: "member" });
Getting Full Organization Data
Retrieve the active organization with all its members, invitations, and teams:
const { data } = await authClient.organization.getFullOrganization();
Members
Members are users who belong to an organization. Each member has a role that determines their permissions.
Adding Members (Server-Side)
Add members directly without invitations (useful for admin operations):
await auth.api.addMember({
body: {
userId: "user-id",
role: "member",
organizationId: "org-id",
},
});
Note: For client-side member additions, use the invitation system instead.
Assigning Multiple Roles
Members can have multiple roles for fine-grained permissions:
await auth.api.addMember({
body: {
userId: "user-id",
role: ["admin", "moderator"],
organizationId: "org-id",
},
});
Removing Members
Remove members by ID or email:
await authClient.organization.removeMember({
memberIdOrEmail: "user@example.com",
});
Important: The last owner cannot be removed. Assign the owner role to another member first.
Updating Member Roles
await authClient.organization.updateMemberRole({
memberId: "member-id",
role: "admin",
});
Membership Limits
Control the maximum number of members per organization:
organization({
membershipLimit: async (user, organization) => {
if (organization.metadata?.plan === "enterprise") {
return 1000;
}
return 50;
},
});
Invitations
The invitation system allows admins to invite users via email. Configure email sending to enable invitations.
Setting Up Invitation Emails
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
import { sendEmail } from "./email";
export const auth = betterAuth({
plugins: [
organization({
sendInvitationEmail: async (data) => {
const { email, organization, inviter, invitation } = data;
await sendEmail({
to: email,
subject: `Join ${organization.name}`,
html: `
<p>${inviter.user.name} invited you to join ${organization.name}</p>
<a href="https://yourapp.com/accept-invite?id=${invitation.id}">
Accept Invitation
</a>
`,
});
},
}),
],
});
Sending Invitations
await authClient.organization.inviteMember({
email: "newuser@example.com",
role: "member",
});
Creating Shareable Invitation URLs
For sharing via Slack, SMS, or in-app notifications:
const { data } = await authClient.organization.getInvitationURL({
email: "newuser@example.com",
role: "member",
callbackURL: "https://yourapp.com/dashboard",
});
Note: This endpoint does not call sendInvitationEmail. Handle delivery yourself.
Accepting Invitations
await authClient.organization.acceptInvitation({
invitationId: "invitation-id",
});
Invitation Configuration
organization({
invitationExpiresIn: 60 * 60 * 24 * 7,
invitationLimit: 100,
cancelPendingInvitationsOnReInvite: true,
});
Roles & Permissions
The plugin provides role-based access control (RBAC) with three default roles:
| Role | Description |
|---|
owner | Full access, can delete organization |
admin | Can manage members, invitations, settings |
member | Basic access to organization resources |
Checking Permissions
const { data } = await authClient.organization.hasPermission({
permission: "member:write",
});
if (data?.hasPermission) {
}
Client-Side Permission Checks
For UI rendering without API calls:
const canManageMembers = authClient.organization.checkRolePermission({
role: "admin",
permissions: ["member:write"],
});
Note: For dynamic access control, the client side role permission check will not work. Please use the hasPermission endpoint.
Teams
Teams allow grouping members within an organization.
Enabling Teams
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
teams: {
enabled: true
}
}),
],
});
Creating Teams
const { data } = await authClient.organization.createTeam({
name: "Engineering",
});
Managing Team Members
await authClient.organization.addTeamMember({
teamId: "team-id",
userId: "user-id",
});
await authClient.organization.removeTeamMember({
teamId: "team-id",
userId: "user-id",
});
Active Teams
Similar to active organizations, set an active team for the session:
await authClient.organization.setActiveTeam({
teamId: "team-id",
});
Team Limits
organization({
teams: {
maximumTeams: 20,
maximumMembersPerTeam: 50,
allowRemovingAllTeams: false,
}
});
Dynamic Access Control
For applications needing custom roles per organization at runtime, enable dynamic access control.
Enabling Dynamic Access Control
import { organization } from "better-auth/plugins";
import { dynamicAccessControl } from "@better-auth/organization/addons";
export const auth = betterAuth({
plugins: [
organization({
dynamicAccessControl: {
enabled: true
}
}),
],
});
Creating Custom Roles
await authClient.organization.createRole({
role: "moderator",
permission: {
member: ["read"],
invitation: ["read"],
},
});
Updating and Deleting Roles
await authClient.organization.updateRole({
roleId: "role-id",
permission: {
member: ["read", "write"],
},
});
await authClient.organization.deleteRole({
roleId: "role-id",
});
Note: Pre-defined roles (owner, admin, member) cannot be deleted. Roles assigned to members cannot be deleted until members are reassigned.
Lifecycle Hooks
Execute custom logic at various points in the organization lifecycle:
organization({
hooks: {
organization: {
beforeCreate: async ({ data, user }) => {
return {
data: {
...data,
metadata: { ...data.metadata, createdBy: user.id },
},
};
},
afterCreate: async ({ organization, member }) => {
await createDefaultResources(organization.id);
},
beforeDelete: async ({ organization }) => {
await archiveOrganizationData(organization.id);
},
},
member: {
afterCreate: async ({ member, organization }) => {
await notifyAdmins(organization.id, `New member joined`);
},
},
invitation: {
afterCreate: async ({ invitation, organization, inviter }) => {
await logInvitation(invitation);
},
},
},
});
Schema Customization
Customize table names, field names, and add additional fields:
organization({
schema: {
organization: {
modelName: "workspace",
fields: {
name: "workspaceName",
},
additionalFields: {
billingId: {
type: "string",
required: false,
},
},
},
member: {
additionalFields: {
department: {
type: "string",
required: false,
},
title: {
type: "string",
required: false,
},
},
},
},
});
Security Considerations
Owner Protection
- The last owner cannot be removed from an organization
- The last owner cannot leave the organization
- The owner role cannot be removed from the last owner
Always ensure ownership transfer before removing the current owner:
await authClient.organization.updateMemberRole({
memberId: "new-owner-member-id",
role: "owner",
});
Organization Deletion
Deleting an organization removes all associated data (members, invitations, teams). Prevent accidental deletion:
organization({
disableOrganizationDeletion: true,
});
Or implement soft delete via hooks:
organization({
hooks: {
organization: {
beforeDelete: async ({ organization }) => {
await archiveOrganization(organization.id);
throw new Error("Organization archived, not deleted");
},
},
},
});
Invitation Security
- Invitations expire after 48 hours by default
- Only the invited email address can accept an invitation
- Pending invitations can be cancelled by organization admins
Complete Configuration Example
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
import { sendEmail } from "./email";
export const auth = betterAuth({
plugins: [
organization({
allowUserToCreateOrganization: true,
organizationLimit: 10,
membershipLimit: 100,
creatorRole: "owner",
defaultOrganizationIdField: "slug",
invitationExpiresIn: 60 * 60 * 24 * 7,
invitationLimit: 50,
sendInvitationEmail: async (data) => {
await sendEmail({
to: data.email,
subject: `Join ${data.organization.name}`,
html: `<a href="https://app.com/invite/${data.invitation.id}">Accept</a>`,
});
},
hooks: {
organization: {
afterCreate: async ({ organization }) => {
console.log(`Organization ${organization.name} created`);
},
},
},
}),
],
});