@dariomvg/create
clerkrbacrolespermisosintegracion

Módulo de Roles y Permisos (RBAC) — Clerk (Next.js 16 / App Router)

Módulo autocontenido: no importa nada de los módulos de auth ni de organizations, incluso donde la funcionalidad se superpone (por ejemplo, asignar un rol a un usuario).

Dependencias

  • @clerk/nextjs (ya instalado: no hay nada nuevo)

Mapa de archivos

lib/rbac/
├── service.ts   # Server-only logic. Don't touch.
├── actions.ts   # Server Actions for Server Components ("use server")
├── types.ts     # Role, Permission, OrgRole, AuthResult
└── client.ts    # Show re-export + copy-paste usage examples

No hay un .env.local nuevo (misma instancia/keys de Clerk) ni un proxy.ts nuevo (por las mismas razones que en el módulo de organizations: un solo archivo de middleware por proyecto de Next.js).

Prerrequisito

Organizations tiene que estar habilitado para que los roles/permisos tengan sentido (Dashboard → Organizations settings). Si todavía no lo hiciste, está explicado en el SETUP.md del módulo de organizations.

Advertencias importantes (leelas antes de usar esto en producción)

Los permisos de sistema (org:sys_*) no están en el session token

La propia documentación de Clerk lo advierte: los System Permissions no viajan en los session claims. checkPermission() funciona de forma confiable del lado del servidor para los Custom Permissions, pero si le pasás un valor org:sys_*, el resultado puede no reflejar la realidad en el servidor. Las verificaciones de rol (checkRole()) no tienen este problema: el rol sí está en el session token.

El CRUD del catálogo de roles/permisos usa una API bastante nueva

createRole, updateRole, deleteRole, createPermission, etc. llaman directamente a la Backend API de Clerk (https://api.clerk.com/v1/organization_roles y /organization_permissions), agregada en noviembre de 2025. Al momento de escribir esto no se pudo confirmar un método estable envuelto en el SDK de JS (clerkClient.organizationRoles... o similar), así que service.ts usa un fetch autenticado en lugar de adivinar el nombre de un método. Antes de usar esto en producción, revisá la referencia actual de la Backend API (https://clerk.com/docs/reference/backend-api) por si ahora existe un método envuelto en el SDK: reemplazarlo es un cambio de una línea, y las formas de request/response de acá deberían seguir coincidiendo.

Los Custom Permissions necesitan un Feature primero

Según la documentación de Clerk, un Custom Permission está atado a un Feature dentro de un Role. createPermission() asume que el Feature ya existe: creálo primero en Dashboard → Features (o en la página del Role en Roles & Permissions). Este módulo no gestiona Features.

Los roles/permisos personalizados requieren un plan de pago en producción

Son gratis en desarrollo. En producción requieren el add-on B2B/Enhanced Organizations de Clerk (revisá los precios vigentes cuando lo habilites).

Role Sets

Un rol personalizado no se puede asignar a una organización hasta que esté incluido en el Role Set de esa organización. Configuralo en Dashboard → Organizations settings → Roles & Permissions → Role Sets.

Checklist del Clerk Dashboard (sin código)

  • Organizations habilitado (prerrequisito, mirá arriba)
  • Plan/add-on para roles personalizados en producción, si vas a necesitar más que org:admin/org:member
  • Feature creado por cada Custom Permission que planeás agregar con createPermission()
  • Nuevos roles personalizados agregados a los Role Set(s) correspondientes
  • Creator role / Default role reasignado, si vas a eliminar un rol que actualmente ocupa alguno de los dos

Ejemplos de uso

Crear un rol personalizado y un permiso, y vincularlos:

import { createRole, createPermission, assignPermissionToRole } from "@/lib/rbac/service";

const role = await createRole({ name: "Billing", key: "billing" }); // -> org:billing
const permission = await createPermission({
  name: "Create invoices",
  key: "invoices:create", // -> org:invoices:create (Feature "invoices" must exist)
});
await assignPermissionToRole(role.id, permission.id);

Proteger un Server Component por rol:

import { requireOrgRole } from "@/lib/rbac/service";

export default async function AdminSettingsPage() {
  await requireOrgRole("org:admin", "/dashboard");
  return <p>Admin settings</p>;
}

Proteger una API Route por permiso:

import { checkPermission } from "@/lib/rbac/service";

export async function POST(request: Request) {
  if (!(await checkPermission("org:invoices:create"))) {
    return new Response("Forbidden", { status: 403 });
  }
  // ...
}

Restringir la UI por rol o permiso:

import { Show } from "@/lib/rbac/client";

<Show when={{ role: "org:admin" }}>
  <AdminPanel />
</Show>