Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
587 lines
13 KiB
Markdown
587 lines
13 KiB
Markdown
---
|
|
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: `
|
|
<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`);
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
```
|