better-auth-organization-best-practices について
Better Authのorganizationプラグインを使う開発者向けの設定・運用ガイドです。組織、メンバー、招待、チーム、動的アクセス制御に加え、所有権や削除時の安全策も説明します。
主な機能
- 01組織とクライアントプラグインの設定
- 02組織、メンバー、招待の管理
- 03ロールと権限の設定
- 04チームの有効化とメンバー管理
- 05スキーマとライフサイクルフックのカスタマイズ
活用シーン
- 01組織の作成や作成制限
- 02メンバーの招待とロール割り当て
- 03組織のチームとメンバーの管理
- 04動的権限と削除防止策の設計
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ pedronauck/ skills --skill better-auth-organization-best-practicesインストール後、Claude Code で/better-auth-organization-best-practicesを入力して読み込まれたか確認します。
詳細
セットアップ
- サーバー設定に
organization()プラグインを追加します - クライアント設定に
organizationClient()プラグインを追加します npx @better-auth/cli migrateを実行します- 確認します:データベースに organization、member、invitation テーブルが存在することを確認します
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
}),
],
});
クライアント側のセットアップ
import { createAuthClient } from "better-auth/client";
import { organizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [organizationClient()],
});
組織の作成
作成者には自動的に owner ロールが割り当てられます。
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" },
});
};
組織の作成を制御する
ユーザー属性に基づいて、組織を作成できるユーザーを制限します。
organization({
allowUserToCreateOrganization: async (user) => {
return user.emailVerified === true;
},
organizationLimit: async (user) => {
// Premium users get more organizations
return user.plan === "premium" ? 20 : 3;
},
});
ユーザーに代わって組織を作成する
管理者は他のユーザーの組織を作成できます(サーバー側のみ)。
await auth.api.createOrganization({
body: {
name: "Client Organization",
slug: "client-org",
userId: "user-id-who-will-be-owner", // `userId` is required
},
});
注:userId パラメーターはセッションヘッダーと併用できません。
アクティブな組織
セッションに保存され、後続の API 呼び出しのスコープとして使用されます。ユーザーが組織を選択した後に設定します。
const setActive = async (organizationId: string) => {
const { data, error } = await authClient.organization.setActive({
organizationId,
});
};
多くのエンドポイントでは、organizationId が指定されていない場合、アクティブな組織が使用されます(listMembers、listInvitations、inviteMember など)。
getFullOrganization() を使用すると、メンバー、招待、チームをすべて含むアクティブな組織を取得できます。
メンバー
メンバーの追加(サーバー側)
await auth.api.addMember({
body: {
userId: "user-id",
role: "member",
organizationId: "org-id",
},
});
クライアント側でメンバーを追加する場合は、代わりに招待システムを使用してください。
複数のロールの割り当て
await auth.api.addMember({
body: {
userId: "user-id",
role: ["admin", "moderator"],
organizationId: "org-id",
},
});
メンバーの削除
removeMember({ memberIdOrEmail }) を使用します。最後のオーナーは削除できません。先に別のメンバーに所有権を割り当ててください。
メンバーのロールの更新
updateMemberRole({ memberId, role }) を使用します。
メンバーシップの上限
organization({
membershipLimit: async (user, organization) => {
if (organization.metadata?.plan === "enterprise") {
return 1000;
}
return 50;
},
});
招待
招待メールの設定
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>
`,
});
},
}),
],
});
招待の送信
await authClient.organization.inviteMember({
email: "[email protected]",
role: "member",
});
共有可能な招待 URL
const { data } = await authClient.organization.getInvitationURL({
email: "[email protected]",
role: "member",
callbackURL: "https://yourapp.com/dashboard",
});
// Share data.url via any channel
このエンドポイントは sendInvitationEmail を呼び出しません。配信はご自身で処理してください。
招待の設定
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
});
ロールと権限
デフォルトのロール:owner(フルアクセス)、admin(メンバー、招待、設定の管理)、member(基本アクセス)。
権限の確認
const { data } = await authClient.organization.hasPermission({
permission: "member:write",
});
if (data?.hasPermission) {
// User can manage members
}
クライアント側の UI 描画(静的な確認のみ)には checkRolePermission({ role, permissions }) を使用します。動的なアクセス制御には hasPermission エンドポイントを使用してください。
チーム
チームの有効化
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
teams: {
enabled: true,
},
}),
],
});
チームの作成
const { data } = await authClient.organization.createTeam({
name: "Engineering",
});
チームメンバーの管理
addTeamMember({ teamId, userId })(メンバーはあらかじめ組織に所属している必要があります)と removeTeamMember({ teamId, userId })(組織には引き続き所属します)を使用します。
setActiveTeam({ teamId }) でアクティブなチームを設定します。
チームの上限
organization({
teams: {
maximumTeams: 20, // Max teams per org
maximumMembersPerTeam: 50, // Max members per team
allowRemovingAllTeams: false, // Prevent removing last team
},
});
動的アクセス制御
動的アクセス制御の有効化
import { organization } from "better-auth/plugins";
import { dynamicAccessControl } from "@better-auth/organization/addons";
export const auth = betterAuth({
plugins: [
organization({
dynamicAccessControl: {
enabled: true,
},
}),
],
});
カスタムロールの作成
await authClient.organization.createRole({
role: "moderator",
permission: {
member: ["read"],
invitation: ["read"],
},
});
updateRole({ roleId, permission }) と deleteRole({ roleId }) を使用します。事前定義されたロール(owner、admin、member)は削除できません。メンバーに割り当てられているロールは、再割り当てするまで削除できません。
ライフサイクルフック
組織のライフサイクルにおけるさまざまなタイミングでカスタムロジックを実行します。
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);
},
},
},
});
スキーマのカスタマイズ
テーブル名やフィールド名をカスタマイズし、追加フィールドを追加します。
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,
},
},
},
},
});
セキュリティに関する考慮事項
オーナーの保護
- 最後のオーナーは組織から削除できません
- 最後のオーナーは組織から脱退できません
- 最後のオーナーからオーナーロールを削除できません
現在のオーナーを削除する前に、必ず所有権を移譲してください。
// Transfer ownership first
await authClient.organization.updateMemberRole({
memberId: "new-owner-member-id",
role: "owner",
});
// Then the previous owner can be demoted or removed
組織の削除
組織を削除すると、関連するすべてのデータ(メンバー、招待、チーム)が削除されます。誤って削除されないようにしてください。
organization({
disableOrganizationDeletion: true, // Disable via config
});
または、フックを使って論理削除を実装します。
organization({
hooks: {
organization: {
beforeDelete: async ({ organization }) => {
// Archive instead of delete
await archiveOrganization(organization.id);
throw new Error("Organization archived, not deleted");
},
},
},
});
招待のセキュリティ
- 招待の有効期限は、デフォルトで48時間です
- 招待を承諾できるのは、招待されたメールアドレスのみです
- 保留中の招待は、組織の管理者がキャンセルできます
完全な設定例
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`);
},
},
},
}),
],
});