chore: consolidate new foundation and archive v1 (#1495)
This commit is contained in:
@@ -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`);
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user