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