organization-best-practices

Original🇺🇸 English
Translated

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

163installs
Added on

NPX Install

npx skill4agent add better-auth/skills organization-best-practices

Tags

Translated version includes tags in frontmatter

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