feat: Complete fleet — 94 skills across 10+ domains
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>
This commit is contained in:
586
skills/organization-best-practices/SKILL.md
Normal file
586
skills/organization-best-practices/SKILL.md
Normal file
@@ -0,0 +1,586 @@
|
||||
---
|
||||
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`);
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
1
skills/organization-best-practices/organization-best-practices
Symbolic link
1
skills/organization-best-practices/organization-best-practices
Symbolic link
@@ -0,0 +1 @@
|
||||
/home/localadmin/src/agent-skills/skills/organization-best-practices/
|
||||
Reference in New Issue
Block a user