Panel de administración con Next.js: guía completa de RBAC desde el diseño hasta la implementación

Mirando el editor, el vigésimo tercer if (user.role === 'admin').
El proyecto de panel de administración que heredé el año pasado tenía el control de permisos del predecesor: comprobaciones de UserRole repartidas en más de veinte archivos. Cada vez que añadíamos un rol, había que buscar y modificar en todo el código. Una vez se nos olvidó un sitio y un usuario normal vio los informes financieros. A las dos de la madrugada me llamaron para arreglar el bug.
En aquella época revisé todas las formas de implementar un panel con Next.js y vi que a casi todos les duele el sistema de permisos. Saben que hay que usar RBAC, pero ¿cómo diseñar las tablas? ¿Cómo escribir el middleware? ¿Cómo generar menús dinámicos? ¿Tablas con Ant Design o shadcn/ui? No hay una respuesta estándar; tropezar es casi inevitable.
Tras dos semanas refactorizando el sistema de permisos, por fin pude dormir tranquilo. Este artículo recoge esa experiencia: desde el diseño de arquitectura RBAC hasta la implementación con middleware en Next.js 15, pasando por menús dinámicos y elección de componentes de tabla. Todo el paquete está aquí.
Diseño del modelo RBAC (por qué así)
Qué es RBAC y por qué lo usa todo el mundo
RBAC significa Role-Based Access Control (control de acceso basado en roles). La idea es muy simple: usuario → rol → permiso → recurso.
¿Por qué no asignar permisos directamente al usuario? Se puede, pero es un lío.
Imagina que entran cinco agentes de soporte. Si vinculas permisos usuario a usuario, configuras a cada uno: ver pedidos, responder comentarios, exportar informes… cinco veces. Con RBAC creas el rol «soporte», enlazas permisos al rol y asignas el rol al nuevo empleado. Una configuración, para siempre.
Lo crucial es el costo de mantenimiento. El product manager dice: «Los de soporte ya no pueden exportar informes, los datos son sensibles». Con RBAC cambias el rol una vez y todos los agentes se actualizan al instante. Con permisos por usuario, toca ir uno a uno; se te escapa uno y tienes un incidente en producción.
Más del 80 % de las aplicaciones SaaS empresariales en el extranjero usan RBAC o variantes. La razón es clara: flexibilidad y mantenibilidad en equilibrio. Más simple que ABAC (control basado en atributos), más flexible que vincular usuario y permiso directamente.
Cómo diseñar la granularidad sin agotarte
La granularidad de permisos es un arte. Si es muy gruesa, no controlas bien; si es muy fina, el mantenimiento se dispara.
Mi experiencia: tres niveles.
Permisos a nivel de página (rutas)
- Lo más básico: si el usuario puede entrar en una página
- Por ejemplo, solo administradores en
/admin/users - Con middleware de Next.js; lo veremos en detalle
Permisos a nivel de módulo (menú)
- Qué ítems muestra la barra lateral
- El usuario no ve menús sin acceso; mejor experiencia
- El frontend filtra la configuración del menú según permisos
Permisos a nivel de acción (botones)
- Hasta botones concretos
- Por ejemplo, «Eliminar usuario» solo para superadmin
- Úsalo con cuidado; no hace falta en todos los botones
He visto lo peor: permiso por columna en cada tabla. Configuración infernal y rendimiento pésimo. Regla: no sobre-diseñes.
Para nombrar permisos recomiendo resource:action:
user:create— crear usuarioorder:delete— eliminar pedidoreport:export— exportar informe
Queda claro y facilita ordenar y buscar.
Diseño de tablas en la base de datos
Cuatro tablas centrales: usuarios, roles, permisos, recursos. Dos tablas de relación para muchos a muchos.
// Tabla de usuarios
User {
id: string
name: string
email: string
// Otros datos del usuario
}
// Tabla de roles
Role {
id: string
name: string // "Administrador", "Soporte", "Operaciones"
code: string // "admin", "service", "operator"
description: string
}
// Tabla de permisos
Permission {
id: string
name: string // "Crear usuario"
code: string // "user:create"
resource: string // "user"
action: string // "create"
}
// Tabla de recursos (opcional, según complejidad)
Resource {
id: string
name: string // "Gestión de usuarios"
code: string // "user"
type: string // "page" | "api" | "menu"
}
// Tabla usuario-rol
UserRole {
userId: string
roleId: string
}
// Tabla rol-permiso
RolePermission {
roleId: string
permissionId: string
}
¿Por qué no un roleId en User? Porque un usuario puede tener varios roles.
Zhang es «responsable técnico» y «revisor de contenido»; hay que fusionar permisos. La tabla intermedia lo resuelve; luego un JOIN en la consulta.
Si hay organigrama (departamento, puesto), añade Department y Position. No construyas todas las tablas el primer día: extiende según necesidad. Con Prisma u otro ORM, añadir campos y tablas después es sencillo.
Middleware de Next.js para proteger rutas (núcleo técnico)
Por qué hace falta middleware
Al principio metí lógica de permisos en cada página. Algo así:
// ❌ Mal ejemplo
export default function UsersPage() {
const { user } = useSession()
if (!user) {
redirect('/login')
}
if (user.role !== 'admin') {
return <div>Sin acceso</div>
}
return <div>Lista de usuarios...</div>
}
¿Parece bien? Problemas:
- Repetir en cada página, copy-paste infinito
- Fácil olvidar una página; riesgo de seguridad
- La página ya se renderizó; el usuario ve parpadeos
- En SSR la lógica se complica más
El middleware de Next.js lo resuelve: se ejecuta antes de la página, intercepta y trata todo de forma uniforme. Mejor rendimiento, código limpio, menos mantenimiento.
Implementación completa de middleware.ts
En Next.js 15 el middleware va en middleware.ts en la raíz. Aquí uso NextAuth; puedes usar Clerk u otro.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { getToken } from 'next-auth/jwt'
// Mapa de permisos por ruta
const ROUTE_PERMISSIONS = {
'/admin': ['admin'], // Solo rol admin
'/admin/users': ['admin', 'operator'], // admin y operator
'/dashboard': ['admin', 'operator', 'viewer'], // tres roles
'/reports': ['admin'],
} as const
// Rutas públicas sin login
const PUBLIC_ROUTES = ['/login', '/register', '/forgot-password']
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 1. Rutas públicas: pasar
if (PUBLIC_ROUTES.includes(pathname)) {
return NextResponse.next()
}
// 2. Obtener sesión
const token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET,
})
// 3. Sin login: redirigir a login
if (!token) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', pathname) // Guardar origen para volver tras login
return NextResponse.redirect(loginUrl)
}
// 4. Comprobar permiso de ruta
const userRole = token.role as string
const requiredRoles = ROUTE_PERMISSIONS[pathname as keyof typeof ROUTE_PERMISSIONS]
if (requiredRoles && !requiredRoles.includes(userRole)) {
// Sin permiso: 403
return NextResponse.rewrite(new URL('/403', request.url))
}
// 5. OK: continuar
return NextResponse.next()
}
// Matcher del middleware
export const config = {
matcher: [
// Todas las rutas excepto estáticos y API (ajusta según necesidad)
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
Puntos clave:
Mapa de rutas: constante clara; nueva ruta, una línea aquí.
Lista blanca pública: login, registro, etc., para evitar bucles (usuario quiere entrar y el middleware lo manda otra vez al login…).
Origen tras login: loginUrl.searchParams.set('from', pathname) importa. Tras bloqueo en /admin/users, tras login debe volver ahí, no al inicio.
Sin permiso: uso NextResponse.rewrite, no redirect; la URL no cambia pero el contenido es 403. También puedes redirigir a una página dedicada.
Coordinación frontend y backend
Importante: el middleware es la primera línea; la API debe validar otra vez.
La comprobación en frontend es optimización de UX. El código del navegador se puede alterar; con DevTools se saltan checks. La defensa real está en el servidor.
En Server Actions y rutas API de Next.js, valida permisos de nuevo:
// app/actions/deleteUser.ts
'use server'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
export async function deleteUser(userId: string) {
// Validar permiso otra vez
const session = await auth()
if (!session || session.user.role !== 'admin') {
throw new Error('No autorizado para esta acción')
}
// Eliminar
await db.user.delete({ where: { id: userId } })
return { success: true }
}
Doble capa:
- Middleware frontend: feedback rápido, no mostrar páginas prohibidas
- Validación backend: seguridad real ante peticiones maliciosas
Algunos equipos extraen la config de permisos a un módulo compartido; en monorepo encaja muy bien.
Rendimiento: dónde guardar permisos
¿Consultar la base en cada petición? No. Muy lento.
Dos enfoques:
Opción 1: permisos en el JWT
// Callbacks de NextAuth
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role
token.permissions = user.permissions // Lista de permisos en el token
}
return token
}
}
Ventaja: el middleware no consulta BD. Inconveniente: cambios de permiso hasta que expire el token. Sirve si los permisos cambian poco.
Opción 2: caché Redis
Si cambian a menudo, cachea permisos en Redis. Rápido y casi en tiempo real; una dependencia más.
En mi proyecto usé la opción 1, token de 1 hora. Tras cambiar permisos, pedimos re-login. Los ajustes de permiso no son tan frecuentes.
Menús dinámicos y permisos (clave de UX)
Estructura de configuración del menú
Lo central: filtrar ítems según permisos del usuario. Primero config completa; luego filtro dinámico.
Mi config se ve así:
// config/menu.ts
import { Home, Users, Settings, FileText } from 'lucide-react'
export interface MenuItem {
key: string
label: string
icon: React.ComponentType
path?: string
permission?: string // Permiso requerido
children?: MenuItem[]
}
export const MENU_CONFIG: MenuItem[] = [
{
key: 'dashboard',
label: 'Panel',
icon: Home,
path: '/dashboard',
// Sin permission: visible para cualquier usuario autenticado
},
{
key: 'users',
label: 'Usuarios',
icon: Users,
permission: 'user:read',
children: [
{
key: 'users-list',
label: 'Lista de usuarios',
path: '/admin/users',
permission: 'user:read',
},
{
key: 'users-roles',
label: 'Roles',
path: '/admin/roles',
permission: 'role:read',
},
],
},
{
key: 'reports',
label: 'Informes',
icon: FileText,
path: '/reports',
permission: 'report:read',
},
{
key: 'settings',
label: 'Configuración',
icon: Settings,
path: '/settings',
permission: 'system:config',
},
]
Notas:
¿Plano o árbol? Elijo árbol: relaciones claras, render recursivo. Plano con parentKey también vale.
permission opcional: sin él, todos los autenticados ven el ítem (p. ej. panel base).
Iconos como componentes, no strings: lucide-react, tipado y render sencillo.
Algoritmo de filtrado
Trampa: padre sin permiso pero hijo con permiso.
Sin user:read pero con role:read, ¿mostramos «Usuarios»?
Mi regla: si algún hijo es visible, mostramos el padre.
// lib/menu.ts
export function filterMenuByPermissions(
menuItems: MenuItem[],
userPermissions: string[]
): MenuItem[] {
return menuItems
.map((item) => {
const filteredChildren = item.children
? filterMenuByPermissions(item.children, userPermissions)
: undefined
const hasPermission =
!item.permission || userPermissions.includes(item.permission)
const hasVisibleChildren =
filteredChildren && filteredChildren.length > 0
if (!hasPermission && !hasVisibleChildren) {
return null
}
return {
...item,
children: filteredChildren,
}
})
.filter((item): item is MenuItem => item !== null)
}
Recursivo y claro. Pocos ítems de menú; rendimiento sobra.
Uso en componentes
Lo encapsulo en un Hook:
// hooks/usePermissionMenu.ts
'use client'
import { useMemo } from 'react'
import { useSession } from 'next-auth/react'
import { filterMenuByPermissions } from '@/lib/menu'
import { MENU_CONFIG } from '@/config/menu'
export function usePermissionMenu() {
const { data: session } = useSession()
const filteredMenu = useMemo(() => {
if (!session?.user?.permissions) {
return []
}
return filterMenuByPermissions(MENU_CONFIG, session.user.permissions)
}, [session?.user?.permissions])
return filteredMenu
}
useMemo evita recalcular en cada render si los permisos no cambian.
En la barra lateral:
// components/Sidebar.tsx
'use client'
import { usePermissionMenu } from '@/hooks/usePermissionMenu'
export function Sidebar() {
const menu = usePermissionMenu()
return (
<nav>
{menu.map((item) => (
<MenuItem key={item.key} item={item} />
))}
</nav>
)
}
Limpio.
Resaltado de ruta y migas
Dos detalles: ruta activa y breadcrumb.
Activo con pathname:
'use client'
import { usePathname } from 'next/navigation'
function MenuItem({ item }: { item: MenuItem }) {
const pathname = usePathname()
const isActive = item.path === pathname
return (
<Link
href={item.path || '#'}
className={isActive ? 'bg-blue-100 text-blue-600' : 'text-gray-700'}
>
<item.icon />
{item.label}
</Link>
)
}
Migas: ruta en el árbol del menú:
// lib/menu.ts
export function getMenuPath(
menuItems: MenuItem[],
targetPath: string,
path: MenuItem[] = []
): MenuItem[] | null {
for (const item of menuItems) {
const currentPath = [...path, item]
if (item.path === targetPath) {
return currentPath
}
if (item.children) {
const result = getMenuPath(item.children, targetPath, currentPath)
if (result) return result
}
}
return null
}
Devuelve la ruta desde la raíz; el componente de migas la pinta.
Rutas dinámicas (/admin/users/123): quita parámetros al comparar; ajusta según tu caso.
Tablas de datos: elección e implementación
Comparativa de tablas en 2026
Un panel sin tablas no es panel. Usuarios, pedidos, logs…
Probé las opciones habituales:
Ant Design Table
- Pros: completo, documentación, comunidad china. Orden, filtro, paginación, filas expandibles, columnas fijas.
- Contras: personalizar estilo cuesta; bundle grande; look fijo.
- Para: paneles clásicos, equipos acostumbrados a Ant Design.
MUI DataGrid
- Pros: Material Design, funciones enterprise (scroll virtual, reordenar columnas).
- Contras: funciones avanzadas de pago (Pro), curva de aprendizaje, estilos difíciles.
- Para: proyectos grandes con presupuesto.
shadcn/ui + TanStack Table
- Pros: sin estilos impuestos, muy personalizable, TypeScript, rendimiento. Componentes bajo tu control.
- Contras: más trabajo inicial en UI.
- Para: proyectos modernos, flexibilidad, equipos que escriben código.
React-Admin
- Pros: CRUD y permisos integrados, listo para usar.
- Contras: acoplado al framework, poca flexibilidad.
- Para: prototipos rápidos, CRUD estándar.
Elegí shadcn/ui + TanStack Table: Tailwind CSS, integración natural, estilos a medida. La API de TanStack separa lógica y UI; cambiar la capa visual no obliga a reescribir la lógica.
Tabla con shadcn/ui en detalle
Data Table no es un componente cerrado: enseña a montarlo. Núcleo TanStack Table; shadcn/ui da la UI base.
Instalar:
npx shadcn@latest add table
npm install @tanstack/react-table
Crea un DataTable (código completo en el ejemplo del artículo).
Uso: defines columnas:
// app/admin/users/page.tsx
'use client'
import { ColumnDef } from '@tanstack/react-table'
import { DataTable } from '@/components/DataTable'
import { Button } from '@/components/ui/button'
import { usePermission } from '@/hooks/usePermission'
interface User {
id: string
name: string
email: string
role: string
}
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Nombre',
},
{
accessorKey: 'email',
header: 'Correo',
},
{
accessorKey: 'role',
header: 'Rol',
},
{
id: 'actions',
cell: ({ row }) => {
const user = row.original
const { hasPermission } = usePermission()
return (
<div className="flex gap-2">
{hasPermission('user:update') && (
<Button size="sm" variant="outline">
Editar
</Button>
)}
{hasPermission('user:delete') && (
<Button size="sm" variant="destructive">
Eliminar
</Button>
)}
</div>
)
},
},
]
export default function UsersPage() {
const users: User[] = [
{ id: '1', name: 'Zhang San', email: '[email protected]', role: 'admin' },
{ id: '2', name: 'Li Si', email: '[email protected]', role: 'user' },
]
return (
<div className="container mx-auto py-10">
<DataTable columns={columns} data={users} />
</div>
)
}
En actions, usePermission controla qué botones ve cada usuario.
Paginación y filtros en servidor
El ejemplo anterior pagina en cliente; con muchos datos no sirve.
En producción, paginación en servidor. API típica:
// app/api/users/route.ts
import { NextRequest } from 'next/server'
import { db } from '@/lib/db'
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const page = parseInt(searchParams.get('page') || '0')
const size = parseInt(searchParams.get('size') || '10')
const [data, total] = await Promise.all([
db.user.findMany({
skip: page * size,
take: size,
}),
db.user.count(),
])
return Response.json({ data, total })
}
No olvides validar permisos en la API.
Buenas prácticas de permisos en tablas
Dos niveles:
Columnas: solo ciertos roles ven datos sensibles (teléfono, DNI)
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Nombre',
},
...(hasPermission('user:view-sensitive')
? [
{
accessorKey: 'phone',
header: 'Teléfono',
},
]
: []),
]
Acciones: botones según permiso (ejemplo anterior con usePermission).
Encapsula un Hook reutilizable:
// hooks/usePermission.ts
'use client'
import { useSession } from 'next-auth/react'
export function usePermission() {
const { data: session } = useSession()
const hasPermission = (permission: string) => {
return session?.user?.permissions?.includes(permission) ?? false
}
const hasAnyPermission = (permissions: string[]) => {
return permissions.some((p) => hasPermission(p))
}
const hasAllPermissions = (permissions: string[]) => {
return permissions.every((p) => hasPermission(p))
}
return { hasPermission, hasAnyPermission, hasAllPermissions }
}
Uso uniforme en componentes.
Producción: precauciones y buenas prácticas
Errores y antipatrones
Trampas que yo pisé:
❌ Error 1: solo permisos en frontend
Lo más peligroso. Todo corre en el navegador; DevTools cambia lo que quieras.
Un analista de la competencia se registró como usuario normal y cambió role: 'user' a role: 'admin'. Pasó la noche mirando nuestro panel. Al día siguiente el product manager no tenía buen color.
✅ Correcto: frontend es UX; la API debe validar. Cada acción sensible en Server Actions o rutas API.
❌ Error 2: if (user.role === 'admin') en veinte archivos
Nuevo rol = buscar y reemplazar hasta la extenuación.
✅ Correcto: config central + funciones de permiso. El Hook usePermission va en esa línea.
❌ Error 3: permisos hardcodeados
// Mal ejemplo
const ADMIN_USERS = ['[email protected]', '[email protected]']
if (ADMIN_USERS.includes(user.email)) {
// lógica admin
}
Cambia el correo del jefe y toca desplegar de nuevo.
✅ Correcto: permisos en base de datos, consulta dinámica. Roles y permisos configurables, no en código.
Optimización de rendimiento
Mal hecho, el sistema de permisos penaliza:
1. Permisos en el token
Como antes: rol y lista en JWT, sin consulta por petición.
2. Caché del menú filtrado
Filtrado recursivo: no recalcules en cada render. useMemo en usePermissionMenu.
3. Code splitting por ruta
App Router parte por ruta; cada página es un chunk. Muchas páginas sin splitting = primera carga lenta.
4. Menos comprobaciones redundantes
En una página de solo lectura, si ya entró, los botones pueden comprobar solo permisos incrementales (borrar, editar).
Checklist de seguridad
Antes de publicar:
✅ API valida permisos
- Server Actions con check
- Rutas API con check
- Operaciones sensibles con segunda verificación (p. ej. borrar usuario)
✅ Evitar escalada de privilegios
- El usuario no cambia su propio rol
- No se auto-asigna permisos
- Usuario de bajo nivel no accede a recursos altos
✅ Auditoría
- Registrar acciones clave (crear usuario, borrar datos, cambiar permisos)
- Quién, cuándo, qué
- Logs append-only
✅ Sesión
- Expiración razonable del token (1 h recomendada)
- Cierre forzado de sesiones
- Tras cambio de contraseña, invalidar tokens viejos
✅ Validación de entrada
- Frontend y backend
- Zod u otro esquema
- SQL injection: ORM como Prisma ayuda
Monitorización y alertas
Publicar no es el final:
Métricas:
- Pico de 403 → posible sondeo
- Muchas peticiones de un usuario → bot o ataque
- Cambios frecuentes de permisos → mala gestión
Alertas:
- Acciones de superadmin en tiempo real
- Email al cambiar config de permisos
- SMS en login anómalo (ubicación, hora)
Sentry, DataDog, etc.
Lecciones reales
Caso 1: menú y ruta desalineados
Menú daba «Informes financieros» a operaciones; el middleware no. Entrada visible, clic → 403. Una semana de quejas.
Lección: una sola fuente de verdad para menú y rutas.
Caso 2: caché de permisos
Permisos en JWT, 24 h de vida. Revocamos acceso; el usuario siguió hasta que expiró el token.
Lección: operaciones sensibles no solo token; reconsultar BD o lista negra en Redis.
Caso 3: API sin permiso
Frontend impecable; una API sin check. Postman saltó todo.
Lección: la API es la última línea. Middleware o decoradores uniformes; no confíes en que cada dev recuerde.
En resumen: frontend = UX, backend = seguridad. Los dos, pero el backend manda.
Conclusión
El sistema de permisos no es magia negra, pero hacerlo bien cuesta.
Este artículo va de RBAC a middleware en Next.js 15, menús y tablas. Tres ideas:
- Diseño sin exceso: extiende cuando haga falta, no un modelo monstruo el día uno
- Capas: middleware en rutas, menú filtrado, botones según necesidad
- Doble defensa: frontend para experiencia, backend para seguridad
Si estás montando un panel:
- Cuatro tablas RBAC (usuario, rol, permiso, recurso)
- Middleware con config en constantes
- Menú dinámico en un Hook
- Tablas con shadcn/ui + TanStack Table
Dos semanas de refactor, pero ahora un rol nuevo es config en BD, cero código. «Auditor» lo monté en diez minutos.
Un buen sistema de permisos acelera al equipo. No esperes al incidente.
El proyecto open source HaloLight es referencia: Next.js 15 + React 19 + TypeScript + RBAC. Código sólido, muy útil para aprender.
Si te atas en la implementación, comenta. He pisado la mayoría de las trampas; lo que pueda ayudar, encantado.
Flujo de implementación RBAC en panel Next.js
Pasos completos para montar RBAC en un panel de administración con Next.js desde cero
⏱️ Estimated time: 120 min
- 1
Step 1: Paso 1: Diseñar tablas RBAC
Crear cuatro tablas centrales y dos de relación:
**Tablas centrales**:
• User: datos básicos del usuario
• Role: roles (admin, operator, viewer, etc.)
• Permission: permisos en formato resource:action (p. ej. user:create)
• Resource (opcional): definición de recursos
**Tablas de relación**:
• UserRole: usuario-rol muchos a muchos
• RolePermission: rol-permiso muchos a muchos
**Convención de nombres**:
Permisos en formato resource:action para gestión y búsqueda.
**Extensibilidad**:
Empezar simple; añadir Department y Position si hace falta organigrama.
Usar Prisma u otro ORM para evolucionar el esquema. - 2
Step 2: Paso 2: Middleware de rutas en Next.js
Crear middleware.ts en la raíz:
**Mapa de permisos**:
• Constante ROUTE_PERMISSIONS
• Roles requeridos por ruta
• PUBLIC_ROUTES (login, registro, etc.)
**Lógica del middleware**:
1. Si ruta pública, pasar
2. getToken para sesión
3. Sin login → redirect a login (guardar from)
4. Comprobar rol vs ruta
5. Sin permiso → 403
**Rendimiento**:
• Permisos en JWT
• Evitar consulta BD por petición
• Expiración del token ~1 hora
**matcher**:
Excluir estáticos y API; validar páginas. - 3
Step 3: Paso 3: Menú dinámico y filtrado
Configuración y filtrado:
**Estructura** (config/menu.ts):
• Árbol de menú
• key, label, icon, path, permission
• permission opcional = visible para todos autenticados
**Algoritmo** (lib/menu.ts):
• filterMenuByPermissions recursivo
• Padre visible si algún hijo lo está
• Devolver árbol filtrado
**Hook** (hooks/usePermissionMenu.ts):
• useSession para permisos
• useMemo para cachear
• Sin recalcular si permisos iguales
**Ruta activa y migas**:
• usePathname
• getMenuPath para breadcrumb
• Rutas dinámicas: normalizar parámetros - 4
Step 4: Paso 4: shadcn/ui + TanStack Table
Tabla reutilizable:
**Instalación**:
• npx shadcn@latest add table
• npm install @tanstack/react-table
**DataTable**:
• useReactTable de TanStack
• Orden, paginación, filtro
• Tipos TypeScript
**Permisos en tabla**:
• Columnas sensibles: render condicional
• Acciones: usePermission en botones
• hasPermission, hasAnyPermission, hasAllPermissions
**Paginación servidor**:
• API con page y size
• Prisma skip/take
• Devolver data y total
**Validación**:
Permisos en tabla = UX; la API debe validar otra vez. - 5
Step 5: Paso 5: Validación en API y Server Actions
Defensa en backend:
**Server Actions**:
• auth() al inicio
• Comprobar rol y permisos
• Error si no autorizado
**Rutas API**:
• getToken
• Validar petición
• Segunda verificación en acciones sensibles
**Config unificada**:
• Módulo compartido de permisos
• Misma regla frontend y backend
• Ideal en monorepo
**Auditoría**:
• Registrar crear, borrar, cambiar permisos
• Operador, hora, detalle
• Logs solo append - 6
Step 6: Paso 6: Rendimiento y seguridad
Optimización en producción:
**Rendimiento**:
• Permisos en JWT
• useMemo en menú
• Code splitting por ruta
• Evitar checks redundantes
**Seguridad**:
• Toda API con permiso
• Anti escalada de privilegios
• Expiración de token
• Validación frontend y backend
• Esquemas con Zod
**Monitorización**:
• Contar 403
• Peticiones anómalas
• Alertas superadmin
• Email en cambios de permisos
**Evitar**:
• Solo frontend
• Permisos hardcodeados
• Menú y ruta distintos
• Solo token en operaciones sensibles
FAQ
¿Por qué RBAC y no permisos directos por usuario?
• **Gestión masiva**: cinco agentes de soporte = un rol, no cinco configs
• **Actualización única**: cambias el rol y todos los usuarios se sincronizan
• **Escalable**: un usuario puede tener varios roles; permisos se fusionan
• **Menos errores**: usuario-permiso directo deja sitios sin actualizar
Más del 80 % del SaaS empresarial usa RBAC por el equilibrio flexibilidad/mantenibilidad.
¿Middleware de Next.js vs permisos en componentes?
**Middleware (recomendado)**:
• Antes de la página, interceptación uniforme
• 60-80 % más rápido que en componente
• Código central, difícil olvidar una ruta
• Compatible con SSR
**En componente**:
• Tras render, parpadeos
• Repetir en cada página
• Copy-paste y omisiones
Recuerda: middleware es primera línea; la API debe validar otra vez.
¿Menú padre sin permiso pero hijo con permiso?
**Lógica**:
• Si un hijo es visible, mostrar el padre
• El usuario accede a lo que sí puede
**Implementación**:
Algoritmo recursivo:
1. Filtrar hijos
2. Comprobar permiso del ítem
3. Sin permiso pero hijos visibles → conservar
4. Sin permiso ni hijos → eliminar
Equilibrio entre control y UX.
¿shadcn/ui + TanStack Table o Ant Design Table?
**Ant Design Table**:
• Equipo ya usa Ant Design
• Desarrollo rápido, todo incluido
• Panel enterprise clásico
• Bundle grande aceptable
**shadcn/ui + TanStack Table**:
• Tailwind CSS
• Estilos muy personalizables
• Flexibilidad y rendimiento
• Más código propio
**Datos**:
Entre 2024-2026 el combo shadcn/ui + TanStack creció más del 300 % en paneles modernos.
Ambos son buenos; mira stack y equipo.
¿Cómo coordinar permisos frontend y backend?
**Frontend (middleware + componentes)**:
• Objetivo: UX, feedback rápido
• Middleware en rutas, botones en UI
• Se puede saltar con DevTools; no es la defensa
**Backend (API + Server Actions)**:
• Objetivo: seguridad real
• Cada acción y ruta API
• Obligatorio en operaciones sensibles
**Config unificada**:
• Módulo compartido
• Mismas reglas
• Evita huecos menú/ruta
**Lección**: frontend perfecto, una API sin check → Postman lo explota.
¿Permisos en JWT o consulta a base de datos cada vez?
**JWT (recomendado en muchos casos)**:
• Sin consulta en middleware
• Cambios tras expiración del token
• Permisos estables
• Expiración ~1 hora
**Redis**:
• Casi tiempo real
• Más infraestructura
• Permisos que cambian a menudo
**Consulta BD**:
• 100 % actual
• Lento por petición
• Solo casos especiales
**Híbrido**:
JWT + lista negra Redis de permisos revocados.
¿Checklist de seguridad antes de publicar?
**API**:
• Server Actions con permiso
• Rutas API con permiso
• Segunda verificación en borrados, etc.
**Anti escalada**:
• No auto-cambio de rol
• No auto-asignación de permisos
• Recursos altos protegidos
**Auditoría y monitor**:
• Log de acciones críticas (inmutable)
• Alertas por 403 anómalos
• Superadmin en tiempo real
• Email al cambiar permisos
**Sesión**:
• Token con expiración (~1 h)
• Logout forzado
• Invalidar tokens tras cambio de contraseña
**Entrada**:
• Validación en ambos lados
• Zod
• ORM contra inyección SQL
Frontend = UX; backend = seguridad.
16 min de lectura · Publicado el: 7 ene 2026 · Actualizado el: 21 ago 2026
Guía completa de Next.js
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Guía completa de subida de archivos en Next.js: carga directa con URL pre-firmada en S3/Qiniu Cloud
Aprende a usar URLs pre-firmadas para subir archivos directamente a S3 o Qiniu Cloud desde Next.js, superar el límite de 4 MB, soportar archivos de hasta 5 GB, con ejemplos de código completos, optimización de rendimiento y mejores prácticas para producción.
Parte 38 de 51
Siguiente
Guía completa para desplegar Next.js en Vercel: variables de entorno, dominio personalizado y monitorización de rendimiento
Guía completa para desplegar Next.js en Vercel: configuración de variables de entorno, vinculación de dominio personalizado, certificados SSL y monitorización de rendimiento, evitando los errores más habituales de principiantes.
Parte 40 de 51



Comentarios
Inicia sesión con GitHub para dejar un comentario