organization best practices

📁 better-auth/skills 📅 3 days ago
1
总安装量
341
周安装量
#50289
全站排名
安装命令
npx skills add https://github.com/better-auth/skills --skill organization-best-practices

Agent 安装分布

opencode 280
gemini-cli 278
github-copilot 277
amp 261
kimi-cli 256

Skill 文档

Setting Up Organizations

When adding organizations to your application, configure the organization plugin with appropriate limits and permissions.

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:

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.

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:

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):

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.

const setActive = async (organizationId: string) => {
  const { data, error } = await authClient.organization.setActive({
    organizationId,
  });
};

Many endpoints use the active organization when organizationId is not provided:

// 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:

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):

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:

await auth.api.addMember({
  body: {
    userId: "user-id",
    role: ["admin", "moderator"],
    organizationId: "org-id",
  },
});

Removing Members

Remove members by ID or email:

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

await authClient.organization.updateMemberRole({
  memberId: "member-id",
  role: "admin",
});

Membership Limits

Control the maximum number of members per organization:

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

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

await authClient.organization.inviteMember({
  email: "newuser@example.com",
  role: "member",
});

Creating Shareable Invitation URLs

For sharing via Slack, SMS, or in-app notifications:

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

await authClient.organization.acceptInvitation({
  invitationId: "invitation-id",
});

Invitation Configuration

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

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:

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

import { organization } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    organization({
        teams: {
            enabled: true
        }
    }),
  ],
});

Creating Teams

const { data } = await authClient.organization.createTeam({
  name: "Engineering",
});

Managing Team Members

// 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:

await authClient.organization.setActiveTeam({
  teamId: "team-id",
});

Team Limits

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

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

await authClient.organization.createRole({
  role: "moderator",
  permission: {
    member: ["read"],
    invitation: ["read"],
  },
});

Updating and Deleting Roles

// 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:

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:

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:

// 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:

organization({
  disableOrganizationDeletion: true, // Disable via config
});

Or implement soft delete via hooks:

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

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