.cursor/skills/organization-best-practices/SKILL.md
This skill provides guidance and enforcement rules for implementing multi-tenant organizations, teams, and role-based access control using Better Auth's organization plugin.
npx skillsauth add akghosh111/scyra organization-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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()],
});
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" },
});
};
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;
},
});
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.
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: "[email protected]", role: "member" });
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 are users who belong to an organization. Each member has a role that determines their permissions.
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.
Members can have multiple roles for fine-grained permissions:
await auth.api.addMember({
body: {
userId: "user-id",
role: ["admin", "moderator"],
organizationId: "org-id",
},
});
Remove members by ID or email:
await authClient.organization.removeMember({
memberIdOrEmail: "[email protected]",
});
Important: The last owner cannot be removed. Assign the owner role to another member first.
await authClient.organization.updateMemberRole({
memberId: "member-id",
role: "admin",
});
Control the maximum number of members per organization:
organization({
membershipLimit: async (user, organization) => {
if (organization.metadata?.plan === "enterprise") {
return 1000;
}
return 50;
},
});
The invitation system allows admins to invite users via email. Configure email sending to enable invitations.
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>
`,
});
},
}),
],
});
await authClient.organization.inviteMember({
email: "[email protected]",
role: "member",
});
For sharing via Slack, SMS, or in-app notifications:
const { data } = await authClient.organization.getInvitationURL({
email: "[email protected]",
role: "member",
callbackURL: "https://yourapp.com/dashboard",
});
// Share data.url via any channel
Note: This endpoint does not call sendInvitationEmail. Handle delivery yourself.
await authClient.organization.acceptInvitation({
invitationId: "invitation-id",
});
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
});
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 |
const { data } = await authClient.organization.hasPermission({
permission: "member:write",
});
if (data?.hasPermission) {
// User can manage members
}
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 allow grouping members within an organization.
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
teams: {
enabled: true
}
}),
],
});
const { data } = await authClient.organization.createTeam({
name: "Engineering",
});
// 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",
});
Similar to active organizations, set an active team for the session:
await authClient.organization.setActiveTeam({
teamId: "team-id",
});
organization({
teams: {
maximumTeams: 20, // Max teams per org
maximumMembersPerTeam: 50, // Max members per team
allowRemovingAllTeams: false, // Prevent removing last team
}
});
For applications needing custom roles per organization at runtime, enable 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
}
}),
],
});
await authClient.organization.createRole({
role: "moderator",
permission: {
member: ["read"],
invitation: ["read"],
},
});
// 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.
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);
},
},
},
});
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,
},
},
},
},
});
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
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");
},
},
},
});
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`);
},
},
},
}),
],
});
development
Complete guide for setting up and handling Dodo Payments webhooks for real-time payment event notifications.
development
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
tools
This skill provides guidance and enforcement rules for implementing secure two-factor authentication (2FA) using Better Auth's twoFactor plugin.
documentation
Guide for implementing subscription billing with Dodo Payments - trials, upgrades, downgrades, and on-demand billing.