Configuration
- Ajouter le plugin
organization()à la config du serveur - Ajouter le plugin
organizationClient()à la config du client - Exécuter
npx @better-auth/cli migrate - Vérifier : s'assurer que les tables organization, member, invitation existent dans votre base de données
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
allowUserToCreateOrganization: true,
organizationLimit: 5, // Max orgs par utilisateur
membershipLimit: 100, // Max membres par org
}),
],
});
Configuration côté client
import { createAuthClient } from "better-auth/client";
import { organizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [organizationClient()],
});
Créer des organisations
Le créateur se voit automatiquement attribuer le rôle owner.
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" },
});
};
Contrôler la création d'organisations
Restreindre qui peut créer des organisations selon les attributs de l'utilisateur :
organization({
allowUserToCreateOrganization: async (user) => {
return user.emailVerified === true;
},
organizationLimit: async (user) => {
// Les utilisateurs premium peuvent créer plus d'organisations
return user.plan === "premium" ? 20 : 3;
},
});
Créer des organisations au nom d'autres utilisateurs
Les administrateurs peuvent créer des organisations pour d'autres utilisateurs (côté serveur uniquement) :
await auth.api.createOrganization({
body: {
name: "Client Organization",
slug: "client-org",
userId: "user-id-who-will-be-owner", // `userId` est obligatoire
},
});
Note : Le paramètre userId ne peut pas être utilisé avec les headers de session.
Organisations actives
Stockées dans la session et délimitent les appels API suivants. À définir après que l'utilisateur en sélectionne une.
const setActive = async (organizationId: string) => {
const { data, error } = await authClient.organization.setActive({
organizationId,
});
};
De nombreux endpoints utilisent l'organisation active quand organizationId n'est pas fourni (listMembers, listInvitations, inviteMember, etc.).
Utilisez getFullOrganization() pour récupérer l'org active avec tous les membres, invitations et équipes.
Membres
Ajouter des membres (côté serveur)
await auth.api.addMember({
body: {
userId: "user-id",
role: "member",
organizationId: "org-id",
},
});
Pour ajouter des membres côté client, utilisez le système d'invitation à la place.
Assigner plusieurs rôles
await auth.api.addMember({
body: {
userId: "user-id",
role: ["admin", "moderator"],
organizationId: "org-id",
},
});
Supprimer des membres
Utilisez removeMember({ memberIdOrEmail }). Le dernier owner ne peut pas être supprimé — transférez la propriété à un autre membre d'abord.
Mettre à jour les rôles des membres
Utilisez updateMemberRole({ memberId, role }).
Limites d'adhésion
organization({
membershipLimit: async (user, organization) => {
if (organization.metadata?.plan === "enterprise") {
return 1000;
}
return 50;
},
});
Invitations
Configurer les emails d'invitation
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>
`,
});
},
}),
],
});
Envoyer des invitations
await authClient.organization.inviteMember({
email: "newuser@example.com",
role: "member",
});
URLs d'invitation partageables
const { data } = await authClient.organization.getInvitationURL({
email: "newuser@example.com",
role: "member",
callbackURL: "https://yourapp.com/dashboard",
});
// Partagez data.url via n'importe quel canal
Cet endpoint n'appelle pas sendInvitationEmail — gérez la livraison vous-même.
Configuration des invitations
organization({
invitationExpiresIn: 60 * 60 * 24 * 7, // 7 jours (défaut : 48 heures)
invitationLimit: 100, // Max invitations en attente par org
cancelPendingInvitationsOnReInvite: true, // Annuler les anciennes invites lors d'une réinvitation
});
Rôles et permissions
Rôles par défaut : owner (accès complet), admin (gérer membres/invitations/paramètres), member (accès basique).
Vérifier les permissions
const { data } = await authClient.organization.hasPermission({
permission: "member:write",
});
if (data?.hasPermission) {
// L'utilisateur peut gérer les membres
}
Utilisez checkRolePermission({ role, permissions }) pour le rendu côté client de l'UI (statique uniquement). Pour le contrôle d'accès dynamique, utilisez l'endpoint hasPermission.
Équipes
Activer les équipes
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
teams: {
enabled: true
}
}),
],
});
Créer des équipes
const { data } = await authClient.organization.createTeam({
name: "Engineering",
});
Gérer les membres de l'équipe
Utilisez addTeamMember({ teamId, userId }) (le membre doit d'abord être dans l'org) et removeTeamMember({ teamId, userId }) (reste dans l'org).
Définir l'équipe active avec setActiveTeam({ teamId }).
Limites des équipes
organization({
teams: {
maximumTeams: 20, // Max équipes par org
maximumMembersPerTeam: 50, // Max membres par équipe
allowRemovingAllTeams: false, // Empêcher la suppression de la dernière équipe
}
});
Contrôle d'accès dynamique
Activer le contrôle d'accès dynamique
import { organization } from "better-auth/plugins";
import { dynamicAccessControl } from "@better-auth/organization/addons";
export const auth = betterAuth({
plugins: [
organization({
dynamicAccessControl: {
enabled: true
}
}),
],
});
Créer des rôles personnalisés
await authClient.organization.createRole({
role: "moderator",
permission: {
member: ["read"],
invitation: ["read"],
},
});
Utilisez updateRole({ roleId, permission }) et deleteRole({ roleId }). Les rôles prédéfinis (owner, admin, member) ne peuvent pas être supprimés. Les rôles assignés à des membres ne peuvent pas être supprimés tant qu'ils ne sont pas réassignés.
Hooks de cycle de vie
Exécuter une logique personnalisée à différents points du cycle de vie de l'organisation :
organization({
hooks: {
organization: {
beforeCreate: async ({ data, user }) => {
// Valider ou modifier les données avant création
return {
data: {
...data,
metadata: { ...data.metadata, createdBy: user.id },
},
};
},
afterCreate: async ({ organization, member }) => {
// Logique post-création (ex: envoyer un email de bienvenue, créer des ressources par défaut)
await createDefaultResources(organization.id);
},
beforeDelete: async ({ organization }) => {
// Nettoyage avant suppression
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);
},
},
},
});
Personnalisation du schéma
Personnaliser les noms de tables, les noms de champs et ajouter des champs supplémentaires :
organization({
schema: {
organization: {
modelName: "workspace", // Renommer la table
fields: {
name: "workspaceName", // Renommer les champs
},
additionalFields: {
billingId: {
type: "string",
required: false,
},
},
},
member: {
additionalFields: {
department: {
type: "string",
required: false,
},
title: {
type: "string",
required: false,
},
},
},
},
});
Considérations de sécurité
Protection du propriétaire
- Le dernier owner ne peut pas être supprimé d'une organisation
- Le dernier owner ne peut pas quitter l'organisation
- Le rôle owner ne peut pas être supprimé du dernier owner
Toujours assurer le transfert de propriété avant de supprimer le owner actuel :
// Transférer la propriété d'abord
await authClient.organization.updateMemberRole({
memberId: "new-owner-member-id",
role: "owner",
});
// Ensuite le propriétaire précédent peut être rétrogradé ou supprimé
Suppression d'organisation
Supprimer une organisation supprime toutes les données associées (membres, invitations, équipes). Empêcher la suppression accidentelle :
organization({
disableOrganizationDeletion: true, // Désactiver via config
});
Ou implémenter une soft delete via les hooks :
organization({
hooks: {
organization: {
beforeDelete: async ({ organization }) => {
// Archiver au lieu de supprimer
await archiveOrganization(organization.id);
throw new Error("Organization archived, not deleted");
},
},
},
});
Sécurité des invitations
- Les invitations expirent après 48 heures par défaut
- Seule l'adresse email invitée peut accepter une invitation
- Les invitations en attente peuvent être annulées par les administrateurs de l'organisation
Exemple de configuration complète
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
import { sendEmail } from "./email";
export const auth = betterAuth({
plugins: [
organization({
// Limites d'organisation
allowUserToCreateOrganization: true,
organizationLimit: 10,
membershipLimit: 100,
creatorRole: "owner",
// Slugs
defaultOrganizationIdField: "slug",
// Invitations
invitationExpiresIn: 60 * 60 * 24 * 7, // 7 jours
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`);
},
},
},
}),
],
});