mcpField
O
organization-best-practices

organization-best-practices

github.com/better-auth開発22736.7k 回インストール8 週間前に更新
リポジトリ

organization-best-practices について

Better Auth に組織・チーム管理を追加する開発者向けの実装ガイドです。メンバー、招待、ロール、権限と関連するセキュリティ設定を扱います。

主な機能

  1. 01組織作成と上限の設定
  2. 02メンバーとロールの管理
  3. 03招待とURLの生成
  4. 04チーム管理と権限制御
  5. 05ライフサイクルフックとスキーマのカスタマイズ

活用シーン

  1. 01アプリに複数組織の機能を追加
  2. 02メンバー招待とロールの管理
  3. 03組織内のチーム編成
  4. 04削除とアクセスルールのカスタマイズ

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/better-auth/skills --skill organization-best-practices

インストール後、Claude Code で/organization-best-practicesを入力して読み込まれたか確認します。

詳細

セットアップ

  1. サーバー設定にorganization()プラグインを追加します
  2. クライアント設定にorganizationClient()プラグインを追加します
  3. npx auth@latest migrate(組み込みアダプター)を実行するか、Drizzle/Prisma 用に生成してプッシュします
  4. 確認します:データベースに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`);
          },
        },
      },
    }),
  ],
});

コメント