organization-best-practices

Configurez des organisations multi-tenant, gérez les membres et les invitations, définissez des rôles et des permissions personnalisés, créez des équipes et implémentez le RBAC à l'aide du plugin d'organisation de Better Auth. À utiliser lorsque les utilisateurs ont besoin de configurer des organisations, de gérer des équipes, de définir des rôles de membres, de contrôler les accès ou d'utiliser le plugin d'organisation de Better Auth.

npx skills add https://github.com/better-auth/skills --skill organization-best-practices

Configuration

  1. Ajouter le plugin organization() à la config du serveur
  2. Ajouter le plugin organizationClient() à la config du client
  3. Exécuter npx @better-auth/cli migrate
  4. 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`);
          },
        },
      },
    }),
  ],
});

Skills similaires