better-auth-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, // 单用户最多可创建的组织数量
      membershipLimit: 100, // 单个组织最多可容纳的成员数量
    }),
  ],
});

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) => {
    // 高级版用户可创建更多组织
    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`为必填参数
  },
});
注意
userId
参数不可与会话请求头同时使用。

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}">
              接受邀请
            </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",
});

// 可通过任意渠道分享data.url
该接口不会触发
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天,默认48小时
  invitationLimit: 100, // 单个组织最多可存在的待处理邀请数量
  cancelPendingInvitationsOnReInvite: true, // 重新发送邀请时取消旧的待处理邀请
});

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) {
  // 用户拥有成员管理权限
}
客户端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, // 单个组织最多可创建的团队数量
    maximumMembersPerTeam: 50, // 单个团队最多可容纳的成员数量
    allowRemovingAllTeams: false, // 禁止移除最后一个团队
  },
});

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 }) => {
        // 组织创建前校验或修改数据
        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, `新成员加入`);
      },
    },
    invitation: {
      afterCreate: async ({ invitation, organization, inviter }) => {
        await logInvitation(invitation);
      },
    },
  },
});

Schema Customization

表结构自定义

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", // 重命名表名
      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:
ts
// Transfer ownership first
await authClient.organization.updateMemberRole({
  memberId: "new-owner-member-id",
  role: "owner",
});

// Then the previous owner can be demoted or removed
  • 不可移除组织的最后一位所有者
  • 最后一位所有者不可退出组织
  • 不可移除最后一位所有者的所有者角色
在移除当前所有者前请先完成所有权转让:
ts
// 先转让所有权
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:
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, // 通过配置禁用组织删除功能
});
也可通过钩子实现软删除:
ts
organization({
  hooks: {
    organization: {
      beforeDelete: async ({ organization }) => {
        // 归档而非删除
        await archiveOrganization(organization.id);
        throw new Error("组织已归档,未删除");
      },
    },
  },
});

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({
      // 组织限制
      allowUserToCreateOrganization: true,
      organizationLimit: 10,
      membershipLimit: 100,
      creatorRole: "owner",

      // Slug配置
      defaultOrganizationIdField: "slug",

      // 邀请配置
      invitationExpiresIn: 60 * 60 * 24 * 7, // 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}">接受邀请</a>`,
        });
      },

      // 钩子配置
      hooks: {
        organization: {
          afterCreate: async ({ organization }) => {
            console.log(`组织 ${organization.name} 已创建`);
          },
        },
      },
    }),
  ],
});