chore: consolidate new foundation and archive v1 (#1495)

This commit is contained in:
2026-09-07 12:32:57 -05:00
3511 changed files with 727899 additions and 10 deletions
@@ -0,0 +1,584 @@
---
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: '[email protected]', 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: '[email protected]',
});
```
**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: '[email protected]',
role: 'member',
});
```
### Creating Shareable Invitation URLs
For sharing via Slack, SMS, or in-app notifications:
```ts
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.
### 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`);
},
},
},
}),
],
});
```