Módulo de Organizations/Teams — Clerk (Next.js 16 / App Router)
Es un módulo separado del módulo de auth. Los roles/permisos personalizados viven en tu otro módulo, también separado: este solo usa los roles nativos de organización de Clerk (org:admin / org:member).
Dependencias
@clerk/nextjs(ya instalado para el módulo de auth: no hay nada nuevo)
Mapa de archivos
lib/organizations/
├── service.ts # Server-only logic. Don't touch.
├── actions.ts # Server Actions for Server Components ("use server")
├── types.ts # OrgSummary, OrgMember, OrgInvitation, OrgRole
└── client.ts # Client component/hook re-exports + copy-paste examples
No hay un .env.local nuevo (misma instancia/keys de Clerk que el módulo de auth) ni un proxy.ts nuevo (Next.js solo permite un archivo de middleware; mirá "Protección de rutas" más abajo).
| Necesito... | Ir a |
|---|---|
| Crear/actualizar/eliminar una organización | lib/organizations/service.ts |
| Llamar acciones de organización desde un Server Component (formulario, botón) | lib/organizations/actions.ts |
| Agregar un tipo para algo relacionado con organizaciones | lib/organizations/types.ts |
Renderizar <OrganizationSwitcher/>, cambiar la organización activa, aceptar invitaciones | lib/organizations/client.ts |
| Mostrar/ocultar UI según el rol o permiso en la organización | lib/clerk/client.ts → Show (módulo de auth, no se duplica acá) |
Prerrequisito: habilitar Organizations
Nada de esto funciona hasta que hagas esto en el Dashboard:
- Dashboard → Organizations settings → Enable Organizations.
- Elegí el Membership mode:
Membership required— solo B2B, sin cuentas personales; después de iniciar sesión, los usuarios pasan por una pantalla de "elegir organización".Membership optional— las cuentas personales siguen disponibles junto con las organizaciones (B2C + B2B).
- Creator role — qué rol recibe automáticamente un usuario cuando crea una organización. Por defecto es
org:admin(no existe un rol "Owner" separado; mirá más abajo).
Checklist del Clerk Dashboard (sin código)
- Organizations habilitado + Membership mode elegido
- Creator role confirmado (por defecto
org:admin) - Límite de miembros (
maxAllowedMemberships) definido, si querés un tope estricto; también se puede pasar por organización encreateOrganization() - Roles personalizados más allá de
admin/member— no acá, eso corresponde a tu módulo separado de roles/permisos - Dominios verificados / Enterprise SSO — no están cubiertos por este módulo
Sobre los roles: Owner = Admin
Clerk trae dos roles de sistema: org:admin y org:member. En todo este módulo se usa org:admin como equivalente de Owner; no hay un rol personalizado.
Sobre "rechazar invitación"
La API de Clerk solo ofrece:
accept()— del lado del cliente, la llama el invitado sobre suUserOrganizationInvitationpendienterevoke()— del lado del servidor, la llama un admin para cancelar una invitación pendiente
revokeInvitationAction en este módulo cubre eso.
Protección de rutas
La protección a nivel de organización ocurre por completo a nivel de Server Component/Server Action/Route Handler, con el mismo patrón que requireUser() en el módulo de auth: no hay una alternativa a nivel de middleware a la que recurrir, ya que Clerk deprecó createRouteMatcher() y ya no recomienda bloquear rutas (por path o por rol) en proxy.ts.
import { requireOrganization, requireOrgRole } from "@/lib/organizations/service";
export default async function OrgSettingsPage() {
const org = await requireOrgRole("org:admin"); // redirects if no active org or not admin
return <p>Settings for {org.name}</p>;
}
Ejemplos de uso
Listar las organizaciones del usuario y dejar que cambie entre ellas:
import { OrganizationSwitcher } from "@/lib/organizations/client";
<OrganizationSwitcher hidePersonal />
Invitar a un miembro desde una Server Action:
import { inviteMemberAction } from "@/lib/organizations/actions";
<form action={() => inviteMemberAction(orgId, email, "org:member")}>
...
</form>
Aceptar una invitación pendiente (del lado del cliente, en la lista propia del invitado):
"use client";
import { useOrganizationList } from "@/lib/organizations/client";
const { userInvitations } = useOrganizationList({
userInvitations: { infinite: true },
});
Restringir una sección por rol o permiso:
import { Show } from "@/lib/clerk/client";
<Show when={{ role: "org:admin" }}>
<AdminPanel />
</Show>