organization-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Setup

安装

  1. Add
    organization()
    plugin to server config
  2. Add
    organizationClient()
    plugin to client config
  3. Run
    npx @better-auth/cli migrate
  4. Verify: check that organization, member, invitation tables exist in your database
ts
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
    }),
  ],
});
  1. 在服务端配置中添加
    organization()
    插件
  2. 在客户端配置中添加
    organizationClient()
    插件
  3. 运行
    npx @better-auth/cli migrate
  4. 验证:检查数据库中是否存在 organization、member、invitation 表
ts
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
    }),
  ],
});

Client-Side Setup

客户端安装

ts
import { createAuthClient } from "better-auth/client";
import { organizationClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [organizationClient()],
});
ts
import { createAuthClient } from "better-auth/client";
import { organizationClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [organizationClient()],
});

Creating Organizations

创建组织

The creator is automatically assigned the
owner
role.
ts
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" },
  });
};
创建者会自动被分配
owner
角色。
ts
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:
ts
organization({
  allowUserToCreateOrganization: async user => {
    return user.emailVerified === true;
  },
  organizationLimit: async user => {
    // Premium users get more organizations
    return user.plan === "premium" ? 20 : 3;
  },
});
可根据用户属性限制可创建组织的用户:
ts
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):
ts
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.
管理员可替其他用户创建组织(仅服务端可用):
ts
await auth.api.createOrganization({
  body: {
    name: "Client Organization",
    slug: "client-org",
    userId: "user-id-who-will-be-owner", // `userId` is required
  },
});
注意
userId
参数不可与会话 header 同时使用。

Active Organizations

活跃组织

Stored in the session and scopes subsequent API calls. Set after user selects one.
ts
const setActive = async (organizationId: string) => {
  const { data, error } = await authClient.organization.setActive({
    organizationId,
  });
};
Many endpoints use the active organization when
organizationId
is not provided (
listMembers
,
listInvitations
,
inviteMember
, etc.).
Use
getFullOrganization()
to retrieve the active org with all members, invitations, and teams.
存储在会话中,会限定后续 API 调用的作用域,用户选择组织后完成设置。
ts
const setActive = async (organizationId: string) => {
  const { data, error } = await authClient.organization.setActive({
    organizationId,
  });
};
当未提供
organizationId
时,很多端点(
listMembers
listInvitations
inviteMember
等)都会默认使用活跃组织。
使用
getFullOrganization()
可以获取包含所有成员、邀请和团队的活跃组织完整信息。

Members

成员

Adding Members (Server-Side)

添加成员(服务端)

ts
await auth.api.addMember({
  body: {
    userId: "user-id",
    role: "member",
    organizationId: "org-id",
  },
});
For client-side member additions, use the invitation system instead.
ts
await auth.api.addMember({
  body: {
    userId: "user-id",
    role: "member",
    organizationId: "org-id",
  },
});
如果需要在客户端添加成员,请改用邀请系统。

Assigning Multiple Roles

分配多个角色

ts
await auth.api.addMember({
  body: {
    userId: "user-id",
    role: ["admin", "moderator"],
    organizationId: "org-id",
  },
});
ts
await auth.api.addMember({
  body: {
    userId: "user-id",
    role: ["admin", "moderator"],
    organizationId: "org-id",
  },
});

Removing Members

移除成员

Use
removeMember({ memberIdOrEmail })
. The last owner cannot be removed — assign ownership to another member first.
使用
removeMember({ memberIdOrEmail })
。最后一位所有者无法被移除——请先将所有权转让给其他成员。

Updating Member Roles

更新成员角色

Use
updateMemberRole({ memberId, role })
.
使用
updateMemberRole({ memberId, role })

Membership Limits

成员数量限制

ts
organization({
  membershipLimit: async (user, organization) => {
    if (organization.metadata?.plan === "enterprise") {
      return 1000;
    }
    return 50;
  },
});
ts
organization({
  membershipLimit: async (user, organization) => {
    if (organization.metadata?.plan === "enterprise") {
      return 1000;
    }
    return 50;
  },
});

Invitations

邀请

Setting Up Invitation Emails

设置邀请邮件

ts
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>
          `,
        });
      },
    }),
  ],
});
ts
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

发送邀请

ts
await authClient.organization.inviteMember({
  email: "newuser@example.com",
  role: "member",
});
ts
await authClient.organization.inviteMember({
  email: "newuser@example.com",
  role: "member",
});

Shareable Invitation URLs

可分享的邀请链接

ts
const { data } = await authClient.organization.getInvitationURL({
  email: "newuser@example.com",
  role: "member",
  callbackURL: "https://yourapp.com/dashboard",
});

// Share data.url via any channel
This endpoint does not call
sendInvitationEmail
— handle delivery yourself.
ts
const { data } = await authClient.organization.getInvitationURL({
  email: "newuser@example.com",
  role: "member",
  callbackURL: "https://yourapp.com/dashboard",
});

// Share data.url via any channel
该端点不会调用
sendInvitationEmail
——请自行处理发送渠道。

Invitation Configuration

邀请配置

ts
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
});
ts
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

角色与权限

Default roles:
owner
(full access),
admin
(manage members/invitations/settings),
member
(basic access).
默认角色:
owner
(全访问权限)、
admin
(可管理成员/邀请/设置)、
member
(基础访问权限)。

Checking Permissions

权限校验

ts
const { data } = await authClient.organization.hasPermission({
  permission: "member:write",
});

if (data?.hasPermission) {
  // User can manage members
}
Use
checkRolePermission({ role, permissions })
for client-side UI rendering (static only). For dynamic access control, use the
hasPermission
endpoint.
ts
const { data } = await authClient.organization.hasPermission({
  permission: "member:write",
});

if (data?.hasPermission) {
  // User can manage members
}
客户端 UI 渲染可使用
checkRolePermission({ role, permissions })
(仅静态校验)。动态访问控制请使用
hasPermission
端点。

Teams

团队

Enabling Teams

启用团队功能

ts
import { organization } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    organization({
      teams: {
        enabled: true,
      },
    }),
  ],
});
ts
import { organization } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    organization({
      teams: {
        enabled: true,
      },
    }),
  ],
});

Creating Teams

创建团队

ts
const { data } = await authClient.organization.createTeam({
  name: "Engineering",
});
ts
const { data } = await authClient.organization.createTeam({
  name: "Engineering",
});

Managing Team Members

管理团队成员

Use
addTeamMember({ teamId, userId })
(member must be in org first) and
removeTeamMember({ teamId, userId })
(stays in org).
Set active team with
setActiveTeam({ teamId })
.
使用
addTeamMember({ teamId, userId })
(成员必须已加入组织)和
removeTeamMember({ teamId, userId })
(成员仍保留在组织中)。
使用
setActiveTeam({ teamId })
设置活跃团队。

Team Limits

团队数量限制

ts
organization({
  teams: {
    maximumTeams: 20, // Max teams per org
    maximumMembersPerTeam: 50, // Max members per team
    allowRemovingAllTeams: false, // Prevent removing last team
  },
});
ts
organization({
  teams: {
    maximumTeams: 20, // Max teams per org
    maximumMembersPerTeam: 50, // Max members per team
    allowRemovingAllTeams: false, // Prevent removing last team
  },
});

Dynamic Access Control

动态访问控制

Enabling Dynamic Access Control

启用动态访问控制

ts
import { organization } from "better-auth/plugins";
import { dynamicAccessControl } from "@better-auth/organization/addons";

export const auth = betterAuth({
  plugins: [
    organization({
      dynamicAccessControl: {
        enabled: true,
      },
    }),
  ],
});
ts
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

创建自定义角色

ts
await authClient.organization.createRole({
  role: "moderator",
  permission: {
    member: ["read"],
    invitation: ["read"],
  },
});
Use
updateRole({ roleId, permission })
and
deleteRole({ roleId })
. Pre-defined roles (owner, admin, member) cannot be deleted. Roles assigned to members cannot be deleted until reassigned.
ts
await authClient.organization.createRole({
  role: "moderator",
  permission: {
    member: ["read"],
    invitation: ["read"],
  },
});
可使用
updateRole({ roleId, permission })
deleteRole({ roleId })
。预定义角色(owner、admin、member)不可删除。已分配给成员的角色在重新分配前不可删除。

Lifecycle Hooks

生命周期钩子

Execute custom logic at various points in the organization lifecycle:
ts
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);
      },
    },
  },
});
在组织生命周期的各个节点执行自定义逻辑:
ts
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

Schema 自定义

Customize table names, field names, and add additional fields:
ts
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,
        },
      },
    },
  },
});
自定义表名、字段名,添加额外字段:
ts
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:
ts
// Transfer ownership first
await authClient.organization.updateMemberRole({
  memberId: "new-owner-member-id",
  role: "owner",
});

// Then the previous owner can be demoted or removed
  • 组织的最后一位所有者无法被移除
  • 最后一位所有者不可退出组织
  • 不可移除最后一位所有者的 owner 角色
在移除当前所有者前,请务必先完成所有权转让:
ts
// 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:
ts
organization({
  disableOrganizationDeletion: true, // Disable via config
});
Or implement soft delete via hooks:
ts
organization({
  hooks: {
    organization: {
      beforeDelete: async ({ organization }) => {
        // Archive instead of delete
        await archiveOrganization(organization.id);
        throw new Error("Organization archived, not deleted");
      },
    },
  },
});
删除组织会移除所有关联数据(成员、邀请、团队)。可通过以下方式防止误删:
ts
organization({
  disableOrganizationDeletion: true, // Disable via config
});
或者通过钩子实现软删除:
ts
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
  • 默认情况下邀请 48 小时后过期
  • 仅被邀请的邮箱地址可接受邀请
  • 组织管理员可取消待处理的邀请

Complete Configuration Example

完整配置示例

ts
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`);
          },
        },
      },
    }),
  ],
});
ts
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`);
          },
        },
      },
    }),
  ],
});