# Organization Best Practices

> This skill provides guidance and enforcement rules for implementing multi-tenant organizations, teams, and role-based access control using Better Auth's organization plugin.

- Skill: `majiayu000/organization-best-practices` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/organization-best-practices`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/organization-best-practices/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/organization-best-practices

---


## 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: `
            <p>${inviter.user.name} invited you to join ${organization.name}</p>
            <a href="https://yourapp.com/accept-invite?id=${invitation.id}">
              Accept Invitation
            </a>
          `,
        });
      },
    }),
  ],
});
```

### 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: `<a href="https://app.com/invite/${data.invitation.id}">Accept</a>`,
        });
      },

      // Hooks
      hooks: {
        organization: {
          afterCreate: async ({ organization }) => {
            console.log(`Organization ${organization.name} created`);
          },
        },
      },
    }),
  ],
});
```

