Organizaciones con Better Auth: multi-tenant paso a paso
Armá organizaciones multi-tenant con el plugin organization de Better Auth: server, cliente, invitaciones y roles. Y cómo lo hace useDeploy sin el plugin.
Para volver multi-tenant una app con Better Auth agregás el plugin organization()
en el server y organizationClient() en el cliente, corrés la migración del
schema y usás authClient.organization.* para crear organizaciones, invitar
miembros y chequear permisos. Con eso tenés organizaciones, miembros,
invitaciones y tres roles por defecto con muy poco código. Esta guía recorre ese
setup con [email protected] y después explica qué hace useDeploy en su
lugar: no usa el plugin, tiene su propio módulo de tenancy montado sobre las
sesiones de Better Auth.
Todas las APIs de abajo siguen la documentación del plugin organization, y los snippets compilan contra 1.6.9.
1. Agregá el plugin en el server
import { betterAuth } from 'better-auth';
import { organization } from 'better-auth/plugins';
export const auth = betterAuth({
// database, emailAndPassword, ...
plugins: [
organization({
allowUserToCreateOrganization: true, // por defecto
creatorRole: 'owner', // por defecto
membershipLimit: 100, // por defecto
sendInvitationEmail: async (data) => {
const link = `https://app.example.com/accept-invitation/${data.id}`;
await sendEmail(data.email, link); // tu proveedor de email
},
}),
],
});Qué hace cada opción, según la doc:
allowUserToCreateOrganizationpuede ser un boolean o una función del usuario, así que podés condicionar la creación de organizaciones a un plan.creatorRolees el rol que recibe quien la crea:owneroadmin.membershipLimitlimita los miembros por organización.sendInvitationEmaillo implementás vos. El plugin te pasa la invitación, armás el link condata.idy mandás el mail.
2. Creá las tablas
El plugin agrega tablas para organizaciones, miembros e invitaciones, además de campos en la sesión que guardan la organización activa. Se generan o aplican con el CLI de Better Auth:
npx auth migrate # aplica el schema a tu base
npx auth generate # escribe el schema para tu ORM en lugar de aplicarloSi usás Prisma, corré generate, integrá el resultado en tu schema.prisma y
hacé la migración de Prisma de siempre.
3. Agregá el plugin del cliente
import { createAuthClient } from 'better-auth/client';
import { organizationClient } from 'better-auth/client/plugins';
export const authClient = createAuthClient({
plugins: [organizationClient()],
});4. Usalo
// crear una organización y pasar a ella
await authClient.organization.create({ name: 'Acme', slug: 'acme' });
await authClient.organization.setActive({ organizationSlug: 'acme' });
// invitar a alguien
await authClient.organization.inviteMember({ email: '[email protected]', role: 'member' });
// quien recibe la invitación la acepta con el id del link del mail
await authClient.organization.acceptInvitation({ invitationId });
// preguntar si el usuario actual puede hacer algo
await authClient.organization.hasPermission({ permissions: { project: ['create'] } });En el server las mismas operaciones viven bajo auth.api, por ejemplo
auth.api.hasPermission({ headers, body: { permissions } }). Cada request se hace
como un usuario con sesión, así que Better Auth sabe quién pregunta.
5. Roles propios con access control
Los roles por defecto son owner (control total), admin (todo menos borrar la
organización o cambiar al owner) y member (solo lectura). Para modelar tus
propios recursos declarás un statement y armás roles a partir de él:
import { createAccessControl } from 'better-auth/plugins/access';
import { defaultStatements, adminAc } from 'better-auth/plugins/organization/access';
const statement = {
...defaultStatements,
project: ['create', 'share', 'update', 'delete'],
} as const;
const ac = createAccessControl(statement);
const member = ac.newRole({ project: ['create'] });
const admin = ac.newRole({ project: ['create', 'update'], ...adminAc.statements });
const owner = ac.newRole({
project: ['create', 'share', 'update', 'delete'],
...adminAc.statements,
});
// pasá los mismos ac y roles a los dos lados
organization({ ac, roles: { owner, admin, member } });
organizationClient({ ac, roles: { owner, admin, member } });Estos roles están definidos en código, así que cambiar uno implica un deploy. La
doc también describe una opción de dynamic access control, respaldada por una
tabla organizationRole, para roles creados en runtime.
Cuándo alcanza el plugin
Usalo cuando quieras multi-tenancy rápido y tus necesidades encajen con su modelo: un conjunto chico y fijo de roles, una organización activa por sesión e invitaciones por email. Lo mantiene el mismo equipo de la librería de auth, así que las actualizaciones van en sincronía.
Cómo lo hace useDeploy
La instancia de Better Auth de useDeploy activa tres plugins, magicLink,
twoFactor y passkey, en
apps/server/src/modules/iam/infrastructure/auth/better-auth.adapter.ts. No activa
organization(). Better Auth se ocupa de la identidad: login, sesiones, 2FA. La
tenancy es otro bounded context, apps/server/src/modules/tenancy, con sus
propios modelos de Prisma: Organization, OrganizationMember, Role e
Invitation.
Lo armamos así por requisitos que el modelo del plugin no cubre de entrada:
- Los roles son datos. Cada organización tiene filas
Rolecon una lista de permisosresource:action. Los owners editan roles en runtime. Los roles de sistema (owner,admin,member) se crean con cada organización y no se pueden editar ni borrar. - La organización viaja con cada request. La organización activa no se guarda
en la sesión. El cliente manda un header
x-organization-iden cada request y un middleware verifica la membresía del usuario en cada una. Scripts y clientes de API pueden apuntar a cualquier organización a la que el usuario pertenezca sin cambiar el estado de la sesión. - Las invariantes viven en el dominio. No se puede degradar ni sacar al último owner; el chequeo corre en una transacción serializable para que dos requests concurrentes no ganen las dos. Un rol solo lo puede otorgar quien ya tiene sus permisos.
- Los tokens de invitación no se guardan. En la base solo está el hash SHA-256 del token.
- Suspensión. Una organización suspendida o borrada se rechaza en cada request, incluso para miembros válidos.
- Las API keys quedan atadas a una organización, con permisos propios acotados.
El costo es que ese código lo mantenés vos, por eso es un módulo de un boilerplate y no algo que te sugeriríamos escribir para un proyecto de fin de semana. El flujo del request, los middlewares y el catálogo de permisos están en RBAC en Express con permisos por organización. El lado de las fugas de datos está en Multi-tenancy in Prisma without RLS (en inglés), y el ángulo para agencias en Reusing one multi-tenant base across every client (también en inglés).
Preguntas frecuentes
¿Necesito el plugin organization para tener multi-tenancy con Better Auth? No. Better Auth solo autentica al usuario. Podés modelar las organizaciones en tus propias tablas, como hace useDeploy, y usar el id del usuario de la sesión para buscar sus membresías.
¿El plugin soporta más de una organización por usuario?
Sí. setActive existe para pasar de una a otra entre las organizaciones a las que
el usuario pertenece; la elegida es la organización activa de la sesión.
¿Dónde vive la organización activa en useDeploy?
En el cliente (un store persistido) y en el header x-organization-id. El server
lo valida contra la tabla de membresías en cada request y nunca confía en él por
sí solo.
¿Para qué versión de Better Auth está escrito esto?
1.6.9, la que usa el package.json de useDeploy. Fijate en la doc por si hay
opciones nuevas.
useDeploy trae Better Auth más el módulo de tenancy descripto arriba, con organizaciones, miembros, invitaciones y roles. Mirá la integración con Better Auth y la feature de multi-tenancy.