--- name: organization-best-practices description: This skill provides guidance and enforcement rules for implementing multi-tenant organizations, teams, and role-based access control using Better Auth's organization plugin. --- ## Setting Up Organizations When adding organizations to your application, configure the `organization` plugin with appropriate limits and permissions. ```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 }), ], }); ``` **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: ```ts 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. ```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; }, }); ``` ### 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. ## 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. ```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: ```ts // 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: ```ts 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): ```ts 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: ```ts await auth.api.addMember({ body: { userId: "user-id", role: ["admin", "moderator"], organizationId: "org-id", }, }); ``` ### Removing Members Remove members by ID or email: ```ts 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 ```ts await authClient.organization.updateMemberRole({ memberId: "member-id", role: "admin", }); ``` ### Membership Limits Control the maximum number of members per organization: ```ts 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 ```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: `

${inviter.user.name} invited you to join ${organization.name}

Accept Invitation `, }); }, }), ], }); ``` ### Sending Invitations ```ts await authClient.organization.inviteMember({ email: "newuser@example.com", role: "member", }); ``` ### Creating Shareable Invitation URLs For sharing via Slack, SMS, or in-app notifications: ```ts 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 ```ts await authClient.organization.acceptInvitation({ invitationId: "invitation-id", }); ``` ### 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 }); ``` ## 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 ```ts 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: ```ts 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 ```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", }); ``` ### Managing Team Members ```ts // 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: ```ts await authClient.organization.setActiveTeam({ teamId: "team-id", }); ``` ### Team Limits ```ts 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 ```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"], }, }); ``` ### Updating and Deleting Roles ```ts // 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: ```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 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, }, }, }, }, }); ``` ## 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 ``` ### 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"); }, }, }, }); ``` ### 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 ```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: `Accept`, }); }, // Hooks hooks: { organization: { afterCreate: async ({ organization }) => { console.log(`Organization ${organization.name} created`); }, }, }, }), ], }); ```