organization best practices
npx skills add https://github.com/better-auth/skills --skill organization-best-practices
Agent 安装分布
Skill 文档
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, // Max orgs per user
membershipLimit: 100, // Max members per org
}),
],
});
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) => {
// Premium users get more organizations
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", // `userId` is required
},
});
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:
// These use the active organization automatically
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();
// data.organization, data.members, data.invitations, data.teams
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",
});
// Share data.url via any channel
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, // 7 days (default: 48 hours)
invitationLimit: 100, // Max pending invitations per org
cancelPendingInvitationsOnReInvite: true, // Cancel old invites when re-inviting
});
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) {
// User can manage members
}
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
// Add a member to a team (must be org member first)
await authClient.organization.addTeamMember({
teamId: "team-id",
userId: "user-id",
});
// Remove from team (stays in org)
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, // Max teams per org
maximumMembersPerTeam: 50, // Max members per team
allowRemovingAllTeams: false, // Prevent removing last team
}
});
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
// Update role permissions
await authClient.organization.updateRole({
roleId: "role-id",
permission: {
member: ["read", "write"],
},
});
// Delete a custom role
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 }) => {
// Validate or modify data before creation
return {
data: {
...data,
metadata: { ...data.metadata, createdBy: user.id },
},
};
},
afterCreate: async ({ organization, member }) => {
// Post-creation logic (e.g., send welcome email, create default resources)
await createDefaultResources(organization.id);
},
beforeDelete: async ({ organization }) => {
// Cleanup before deletion
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", // Rename table
fields: {
name: "workspaceName", // Rename fields
},
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:
// Transfer ownership first
await authClient.organization.updateMemberRole({
memberId: "new-owner-member-id",
role: "owner",
});
// Then the previous owner can be demoted or removed
Organization Deletion
Deleting an organization removes all associated data (members, invitations, teams). Prevent accidental deletion:
organization({
disableOrganizationDeletion: true, // Disable via config
});
Or implement soft delete via hooks:
organization({
hooks: {
organization: {
beforeDelete: async ({ organization }) => {
// Archive instead of delete
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({
// Organization limits
allowUserToCreateOrganization: true,
organizationLimit: 10,
membershipLimit: 100,
creatorRole: "owner",
// Slugs
defaultOrganizationIdField: "slug",
// Invitations
invitationExpiresIn: 60 * 60 * 24 * 7, // 7 days
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
hooks: {
organization: {
afterCreate: async ({ organization }) => {
console.log(`Organization ${organization.name} created`);
},
},
},
}),
],
});