Admin em Next.js na prática: guia completo de RBAC, do design à implementação

Encare o 23º if (user.role === 'admin') aberto no editor.
No ano passado, peguei um projeto de painel administrativo em que o controle de permissões herdado estava espalhado por mais de 20 arquivos. Toda vez que surgia um novo papel, era preciso fazer busca global e alterar um monte de pontos. Em uma dessas, alguém esqueceu um trecho e um usuário comum viu relatórios financeiros. O telefone tocou no meio da noite para corrigir bug.
Naquela fase, vasculhei várias formas de implementar admin em Next.js e percebi que quase todo mundo sofre com sistema de permissões. A gente sabe que precisa de RBAC, mas como desenhar as tabelas? Como escrever o middleware? Como gerar menu dinâmico? Para tabela, Ant Design ou shadcn/ui? Não existe resposta padrão para tudo isso. Algumas armadilhas vêm no pacote.
Depois de duas semanas refatorando o sistema de permissões, finalmente deu para dormir melhor. Este texto organiza a experiência daquela refatoração: do design da arquitetura RBAC à implementação de middleware no Next.js 15, passando por geração dinâmica de menu e escolha de componente de tabela. O caminho completo está aqui.
Design do modelo RBAC de permissões: por que desenhar assim
O que é RBAC e por que tanta gente usa
RBAC significa Role-Based Access Control, ou controle de acesso baseado em papéis. A ideia central é bem simples: usuário → papel → permissão → recurso.
Talvez você pergunte: não dá para atribuir permissões diretamente ao usuário? Dá. Só que dá trabalho.
Imagine que sua empresa acabou de contratar 5 atendentes. Se você vincular permissões diretamente aos usuários, precisa configurar uma por uma: ver pedidos, responder comentários, exportar relatórios… cinco pessoas, cinco configurações. Com RBAC, você cria o papel “atendente”, liga as permissões a esse papel e, quando alguém entra, só atribui o papel. Uma configuração, uso contínuo.
O ponto mais importante é manutenção. O gerente de produto diz: “atendentes não podem mais exportar relatórios, os dados são sensíveis demais”. Com RBAC, você altera uma vez as permissões do papel e todos os atendentes recebem a atualização. Com permissão ligada direto ao usuário? Vai alterar um por um. Esquecer um já vira incidente em produção.
Em aplicações SaaS empresariais no exterior, mais de 80% usam RBAC ou alguma variação. A razão é prática: ele equilibra flexibilidade e manutenibilidade. É mais simples que ABAC, o controle de acesso baseado em atributos, e mais flexível que permissões vinculadas diretamente ao usuário.
Como definir a granularidade sem sofrer
Granularidade de permissão é quase uma arte obscura. Se for ampla demais, o controle fica fraco. Se for fina demais, a manutenção explode.
Minha experiência é dividir em três camadas:
Permissão em nível de página (camada de rota)
- É a base: controla se o usuário pode acessar uma página
- Por exemplo,
/admin/userssó pode ser acessada por administradores - É implementada com middleware do Next.js, como veremos mais adiante
Permissão em nível de módulo (camada de menu)
- Controla quais itens aparecem na barra lateral
- O usuário não vê menus que não pode acessar, o que deixa a experiência mais limpa
- O frontend filtra dinamicamente a configuração de menu com base nas permissões
Permissão em nível de operação (camada de botão)
- Chega ao nível de uma ação específica
- Por exemplo, o botão “Excluir usuário” só aparece para superadministradores
- Use com cuidado: nem todo botão precisa de controle de permissão
Falando bem direto, o caso mais exagerado que já vi foi controle de permissão para cada coluna de cada tabela. O resultado? Configuração complicada demais e desempenho ruim. Guarde uma regra: não projete além do necessário.
Para nomes de permissão, recomendo o formato resource:action:
user:create- criar usuárioorder:delete- excluir pedidoreport:export- exportar relatório
Fica fácil de entender, ordenar e buscar.
Uma estrutura de tabelas que funciona
O núcleo são quatro tabelas: usuário, papel, permissão e recurso. Depois entram duas tabelas de associação para lidar com relações muitos-para-muitos.
// Tabela de usuários
User {
id: string
name: string
email: string
// Outras informações do usuário
}
// Tabela de papéis
Role {
id: string
name: string // "Administrador", "Atendente", "Operações"
code: string // "admin", "service", "operator"
description: string
}
// Tabela de permissões
Permission {
id: string
name: string // "Criar usuário"
code: string // "user:create"
resource: string // "user"
action: string // "create"
}
// Tabela de recursos (opcional, depende da complexidade do negócio)
Resource {
id: string
name: string // "Gestão de usuários"
code: string // "user"
type: string // "page" | "api" | "menu"
}
// Associação usuário-papel
UserRole {
userId: string
roleId: string
}
// Associação papel-permissão
RolePermission {
roleId: string
permissionId: string
}
Alguém pode perguntar: por que não colocar um roleId direto na tabela User? A resposta é: um usuário pode ter vários papéis.
Por exemplo, João pode ser “líder técnico” e também “revisor de conteúdo”. As permissões dos dois papéis precisam ser combinadas. Com uma tabela intermediária, esse cenário fica natural; na consulta, basta fazer o JOIN.
Se o seu negócio também envolve estrutura organizacional, como departamento e cargo, você pode adicionar tabelas Department e Position. Mas não crie tudo logo de início. Expandir conforme a necessidade é o caminho saudável. Com um ORM como Prisma, adicionar campos e tabelas depois é bem tranquilo.
Proteção de rotas com middleware do Next.js: o núcleo técnico
Por que usar middleware
Quando comecei a implementar controle de permissões, escrevia um monte de validação dentro de cada componente de página. Algo assim:
// ❌ Exemplo ruim
export default function UsersPage() {
const { user } = useSession()
if (!user) {
redirect('/login')
}
if (user.role !== 'admin') {
return <div>Sem permissão de acesso</div>
}
return <div>Lista de usuários...</div>
}
Parece ok? O problema aparece rápido:
- É preciso repetir em toda página, uma cópia sem fim
- É fácil esquecer uma página e abrir uma brecha
- A página renderiza antes da validação, então o usuário pode ver uma piscada
- Em renderização no servidor, a lógica fica ainda mais complexa
O middleware do Next.js resolve bem esses pontos. Ele roda antes de a requisição chegar à página, intercepta tudo de forma centralizada e padronizada. O desempenho é bom, o código fica limpo e o custo de manutenção cai.
Implementação completa do middleware.ts
No Next.js 15, o middleware fica no arquivo middleware.ts na raiz do projeto. Aqui uso NextAuth para autenticação, mas você pode trocar por Clerk ou outra solução.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { getToken } from 'next-auth/jwt'
// Define o mapeamento de permissões por rota
const ROUTE_PERMISSIONS = {
'/admin': ['admin'], // Só o papel admin pode acessar
'/admin/users': ['admin', 'operator'], // admin e operator podem acessar
'/dashboard': ['admin', 'operator', 'viewer'], // Três papéis podem acessar
'/reports': ['admin'],
} as const
// Rotas públicas, sem necessidade de login
const PUBLIC_ROUTES = ['/login', '/register', '/forgot-password']
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 1. Libera rotas públicas diretamente
if (PUBLIC_ROUTES.includes(pathname)) {
return NextResponse.next()
}
// 2. Obtém a sessão do usuário
const token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET,
})
// 3. Sem login: redireciona para a tela de login
if (!token) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', pathname) // Registra a origem para voltar após login
return NextResponse.redirect(loginUrl)
}
// 4. Verifica permissão da rota
const userRole = token.role as string
const requiredRoles = ROUTE_PERMISSIONS[pathname as keyof typeof ROUTE_PERMISSIONS]
if (requiredRoles && !requiredRoles.includes(userRole)) {
// Permissão insuficiente: retorna 403
return NextResponse.rewrite(new URL('/403', request.url))
}
// 5. Permissão validada: continua a requisição
return NextResponse.next()
}
// Configura as regras de correspondência do middleware
export const config = {
matcher: [
// Corresponde a todas as rotas, exceto arquivos estáticos e API (ajuste conforme necessário)
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
Alguns pontos importantes:
Mapeamento de permissões por rota: deixo tudo em um objeto constante. Fica visível de cara. Ao adicionar uma rota, basta incluir ali, sem caçar lógica pelo código.
Whitelist de rotas públicas: login, cadastro e rotas sem autenticação ficam separadas. Isso evita loop infinito, como quando o usuário tenta entrar no login e o middleware o manda para o login de novo.
Registro da origem do login: loginUrl.searchParams.set('from', pathname) é um detalhe importante. Se o usuário tentou abrir /admin/users, fez login e foi liberado, ele deve voltar para /admin/users, não para a home.
Tratamento de permissão insuficiente: uso NextResponse.rewrite em vez de redirect; assim a URL não muda, mas o conteúdo vira uma página 403. Você também pode redirecionar para uma página dedicada de acesso negado.
Como combinar validação no frontend e no backend
Aqui está o ponto crítico: middleware é só a primeira barreira; a API do backend precisa validar de novo.
A essência da validação no frontend é otimizar a experiência do usuário. Código no navegador pode ser alterado. Abriu as ferramentas de desenvolvedor, dá para contornar muita coisa. A barreira real de segurança fica no servidor.
Em Server Actions e rotas de API do Next.js, valide permissões novamente:
// app/actions/deleteUser.ts
'use server'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
export async function deleteUser(userId: string) {
// Valida novamente a permissão do usuário
const session = await auth()
if (!session || session.user.role !== 'admin') {
throw new Error('Sem permissão para executar a operação')
}
// Executa a exclusão
await db.user.delete({ where: { id: userId } })
return { success: true }
}
Assim você cria uma proteção em duas camadas:
- Middleware no frontend: feedback rápido e evita que o usuário veja páginas sem permissão
- Validação no backend: barreira real contra requisições maliciosas
Algumas equipes extraem a configuração de permissões para um módulo compartilhado, usado tanto pelo frontend quanto pelo backend. Em monorepo, isso fica especialmente prático.
Otimização de desempenho: onde colocar as permissões
Consultar o banco a cada requisição para obter permissões? Não. Fica lento.
Duas opções:
Opção 1: codificar permissões no JWT
// Callbacks do NextAuth
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role
token.permissions = user.permissions // Inclui a lista de permissões diretamente
}
return token
}
}
A vantagem é que o middleware não precisa consultar o banco. A desvantagem é que, depois de alterar permissões, é preciso esperar o token expirar. Funciona bem quando permissões não mudam com frequência.
Opção 2: cache de permissões em Redis
Se as permissões mudam muito, coloque-as em cache no Redis e consulte o cache pelo middleware. É rápido e mais próximo de tempo real, mas adiciona uma dependência.
No meu projeto, usei a opção 1 e configurei expiração de token em 1 hora. Quando um administrador altera permissões, pede para o usuário fazer login de novo. Afinal, ajuste de permissão não costuma ser uma operação de alta frequência.
Geração dinâmica de menu e vínculo com permissões: a chave da experiência
Estrutura da configuração de menu
O núcleo do menu dinâmico é filtrar itens de menu com base nas permissões do usuário. Primeiro você precisa de uma configuração completa de menu; depois filtra essa configuração de acordo com o usuário atual.
Minha configuração fica assim:
// 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 // Permissão exigida
children?: MenuItem[]
}
export const MENU_CONFIG: MenuItem[] = [
{
key: 'dashboard',
label: 'Dashboard',
icon: Home,
path: '/dashboard',
// Sem permission: qualquer usuário logado pode ver
},
{
key: 'users',
label: 'Gestão de usuários',
icon: Users,
permission: 'user:read', // Exige permissão user:read
children: [
{
key: 'users-list',
label: 'Lista de usuários',
path: '/admin/users',
permission: 'user:read',
},
{
key: 'users-roles',
label: 'Gestão de papéis',
path: '/admin/roles',
permission: 'role:read',
},
],
},
{
key: 'reports',
label: 'Central de relatórios',
icon: FileText,
path: '/reports',
permission: 'report:read',
},
{
key: 'settings',
label: 'Configurações do sistema',
icon: Settings,
path: '/settings',
permission: 'system:config',
},
]
Essa estrutura tem alguns detalhes:
Lista plana ou árvore? Escolhi árvore. A relação de hierarquia fica clara, e a renderização pode ser recursiva. Há quem prefira uma lista plana com parentKey; cada abordagem tem seus custos.
O campo permission é opcional: quando não há permission, qualquer usuário logado vê o item. Uma página básica como “Dashboard” geralmente não precisa de restrição.
Ícone como componente, não string: importo diretamente os componentes de ícone do lucide-react. Fica type-safe e fácil de renderizar.
Algoritmo de filtragem do menu
Com a configuração pronta, vem o núcleo: filtrar o menu pelas permissões do usuário.
Há uma pegadinha: se o menu pai não tem permissão, mas o filho tem, o que fazer?
Por exemplo, o usuário não tem user:read, mas tem role:read. O pai “Gestão de usuários” deve aparecer ou sumir?
Minha estratégia é: se pelo menos um filho estiver visível, mostre o pai. Assim o usuário consegue chegar ao submenu permitido.
// lib/menu.ts
export function filterMenuByPermissions(
menuItems: MenuItem[],
userPermissions: string[]
): MenuItem[] {
return menuItems
.map((item) => {
// Processa submenus
const filteredChildren = item.children
? filterMenuByPermissions(item.children, userPermissions)
: undefined
// Decide se o item atual é visível
const hasPermission =
!item.permission || userPermissions.includes(item.permission)
const hasVisibleChildren =
filteredChildren && filteredChildren.length > 0
// Sem permissão e sem filhos visíveis: remove
if (!hasPermission && !hasVisibleChildren) {
return null
}
// Retorna o item de menu filtrado
return {
...item,
children: filteredChildren,
}
})
.filter((item): item is MenuItem => item !== null)
}
A filtragem recursiva deixa a lógica clara. O desempenho também é tranquilo: um menu administrativo raramente passa de algumas dezenas de itens.
Como usar dentro do componente
Encapsulei a lógica de filtragem em um React Hook para reutilizar com facilidade:
// 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 guarda o resultado e evita recalcular a cada renderização. Se a lista de permissões não muda, o menu filtrado também não muda.
Na sidebar, o uso fica simples:
// 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>
)
}
Direto e limpo.
Destaque de rota e breadcrumbs
Depois de filtrar o menu, ainda restam dois detalhes: destacar a rota atual e gerar breadcrumbs.
O destaque de rota depende de comparar com 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>
)
}
Breadcrumb é um pouco mais trabalhoso: você precisa encontrar, a partir da rota atual, o caminho correspondente dentro da árvore de menu.
// 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
}
A função busca recursivamente e retorna o caminho do nó raiz ao item atual. O componente de breadcrumb só precisa renderizar esse caminho.
Rotas dinâmicas, como /admin/users/123, pedem tratamento especial: na comparação, normalmente você remove a parte dinâmica ou aplica uma regra de match própria. Isso depende do negócio.
Escolha e prática de componentes de tabela: a ferramenta que você usa todo dia
Comparativo das principais opções em 2026
Admin sem tabela não existe. Lista de usuários, pedidos, logs… tudo vira tabela. Escolher uma biblioteca adequada economiza muito tempo.
Testei as opções principais e minha impressão prática foi:
Ant Design Table
- Vantagens: completo, documentação detalhada, amigável para equipes que já conhecem o ecossistema. Ordenação, filtros, paginação, linhas expansíveis e colunas fixas vêm prontos.
- Desvantagens: customizar estilo dá trabalho, o bundle fica grande com o antd inteiro e o visual é bem marcado.
- Serve para: painéis administrativos tradicionais e equipes que já usam Ant Design.
MUI DataGrid
- Vantagens: estilo Material Design, recursos fortes e funcionalidades empresariais como rolagem virtual e reorder de colunas.
- Desvantagens: recursos avançados exigem versão Pro, a curva de aprendizado é alta e sobrescrever estilos pode ser complicado.
- Serve para: projetos grandes, com orçamento e necessidade real de recursos enterprise.
shadcn/ui + TanStack Table
- Vantagens: sem restrição de estilo, altamente customizável, amigável a TypeScript e com ótimo desempenho. Você controla os componentes e importa sob demanda.
- Desvantagens: precisa escrever a UI e os estilos; o investimento inicial é maior.
- Serve para: projetos modernos que priorizam flexibilidade, desempenho e equipes dispostas a escrever código.
React-Admin
- Vantagens: solução integrada, com CRUD e permissões prontos.
- Desvantagens: prende você ao framework e limita customização.
- Serve para: protótipos rápidos e aplicações CRUD padronizadas.
No fim, escolhi shadcn/ui + TanStack Table. O motivo é simples: o projeto já usava Tailwind CSS, então a integração com shadcn/ui foi natural. O estilo fica sob seu controle: quer mudar, muda. Além disso, a API do TanStack Table é muito bem desenhada, separando lógica e UI; trocar a camada visual depois não obriga a reescrever a lógica.
Implementação de tabela com shadcn/ui
O Data Table do shadcn/ui não entrega um componente fechado; ele ensina como montar. O núcleo é o TanStack Table, enquanto o shadcn/ui fornece os componentes básicos de tabela.
Instale as dependências:
npx shadcn@latest add table
npm install @tanstack/react-table
Depois crie um componente DataTable. No uso diário, basta definir as colunas:
// 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: 'Nome',
},
{
accessorKey: 'email',
header: 'E-mail',
},
{
accessorKey: 'role',
header: 'Papel',
},
{
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">
Excluir
</Button>
)}
</div>
)
},
},
]
export default function UsersPage() {
// Em um projeto real, os dados devem vir de uma chamada assíncrona
const users: User[] = [
{ id: '1', name: 'João', email: '[email protected]', role: 'admin' },
{ id: '2', name: 'Maria', email: '[email protected]', role: 'user' },
]
return (
<div className="container mx-auto py-10">
<DataTable columns={columns} data={users} />
</div>
)
}
Observe a coluna actions: uso o Hook usePermission para controlar a exibição dos botões. Usuários com permissões diferentes veem ações diferentes.
Paginação e filtro no servidor
O exemplo anterior usa paginação no cliente, carregando todos os dados no frontend. Com volume maior, isso não funciona.
Em produção, o normal é paginar no servidor. A API pode ficar assim:
// 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 })
}
Não esqueça a validação de permissões, como vimos na seção de middleware.
Boas práticas para controle de permissões em tabelas
Permissões dentro de tabela aparecem em dois níveis:
Permissão de coluna: algumas colunas só podem aparecer para certos papéis, como telefone ou documento.
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Nome',
},
// Só admin pode ver a coluna com informação sensível
...(hasPermission('user:view-sensitive')
? [
{
accessorKey: 'phone',
header: 'Telefone',
},
]
: []),
]
Permissão de ação: botões na coluna de ações aparecem conforme as permissões.
O exemplo anterior já mostrou isso com o Hook usePermission.
O ponto-chave é encapsular um Hook genérico de verificação:
// 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 }
}
Assim os componentes ficam simples e a lógica permanece unificada.
Cuidados em produção e boas práticas: guia para evitar armadilhas
Erros comuns e antipadrões
Aqui vai um resumo das armadilhas que já apareceram na prática, para você não repetir.
❌ Erro 1: validar permissões só no frontend
Esse é o mais perigoso. O código do frontend roda no navegador. Com as ferramentas de desenvolvedor abertas, dá para mexer em muita coisa.
Uma vez, um analista de concorrente se registrou como usuário comum e, pelas ferramentas de desenvolvedor, mudou role: 'user' para role: 'admin'. Passou a noite vendo dados do nosso admin. No dia seguinte, o gerente de produto estava pálido.
✅ Forma correta: validação no frontend é apenas otimização de UX. A API do backend precisa validar de novo. Toda operação sensível deve checar permissão em Server Actions ou rotas de API.
❌ Erro 2: lógica de permissão espalhada por todo lado
if (user.role === 'admin') em 20 arquivos diferentes. Ao adicionar um novo papel, você entra em uma caça interminável.
✅ Forma correta: configuração unificada de permissões + função única de verificação. O Hook usePermission que vimos acima segue essa ideia.
❌ Erro 3: configuração de permissões hardcoded
// Exemplo ruim
const ADMIN_USERS = ['[email protected]', '[email protected]']
if (ADMIN_USERS.includes(user.email)) {
// Lógica de administrador
}
O chefe troca de e-mail e você precisa alterar código e fazer deploy. Não faz sentido.
✅ Forma correta: permissões ficam no banco e são consultadas dinamicamente. A relação entre papéis e permissões também é configuração, não código fixo.
Estratégias de otimização de desempenho
Um sistema de permissões mal feito pode ficar lento. Algumas otimizações ajudam:
1. Codifique permissões no Token
Como vimos, coloque papel e lista de permissões do usuário no JWT para evitar consulta ao banco em toda requisição.
2. Faça cache do menu filtrado
Filtrar menu é uma operação recursiva. Não é complexa, mas não precisa rodar em toda renderização.
No Hook usePermissionMenu, usamos useMemo. Isso é cache. Se a lista de permissões não muda, o resultado do menu filtrado também não é recalculado.
3. Use code splitting por rota
O App Router do Next.js já suporta code splitting por rota. Cada página é um chunk independente, e o usuário só carrega o código da página que acessa.
Admin costuma ter muitas páginas. Sem code splitting, o carregamento inicial fica pesado.
4. Reduza verificações desnecessárias
Algumas equipes fazem permissões finas demais. Em uma página somente leitura, se o usuário já pode acessar a página, talvez não faça sentido validar de novo todos os elementos.
Você pode simplificar: acesso à página garante permissão básica; dentro dela, valide apenas permissões incrementais, como excluir ou editar.
Checklist de segurança
Antes de subir para produção, passe por este checklist:
✅ API de backend precisa validar permissões
- Todas as Server Actions têm verificação de permissão
- Todas as rotas de API têm verificação de permissão
- Operações sensíveis têm segunda validação, como excluir usuário
✅ Prevenção contra privilege escalation
- Usuário não pode alterar o próprio papel
- Usuário não pode adicionar permissões a si mesmo
- Usuário de baixa permissão não acessa recursos de alta permissão
✅ Logs de auditoria
- Registre operações críticas, como criar usuário, excluir dados e alterar permissões
- O log inclui operador, horário e conteúdo da operação
- O log não pode ser adulterado; deve ser apenas append-only
✅ Gestão de sessão
- Token com expiração razoável, como 1 hora
- Suporte a logout forçado, limpando todas as sessões
- Token antigo invalida após troca de senha
✅ Validação de entrada
- Frontend e backend validam entrada
- Use bibliotecas como Zod para definir estruturas de dados
- Evite SQL injection; com ORMs como Prisma, isso já fica naturalmente mais seguro
Monitoramento e alertas
Colocar o sistema de permissões em produção não é o fim. É o começo. Você precisa detectar anomalias cedo:
Métricas de monitoramento:
- Aumento anormal de erros 403 → alguém pode estar testando o sistema
- Muitas requisições de um usuário em pouco tempo → pode ser crawler ou ataque
- Alterações frequentes de permissões → alguém pode estar bagunçando a configuração
Estratégia de alerta:
- Notificação em tempo real para ações de superadministrador
- E-mail quando a configuração de permissões mudar
- SMS para login anormal, como local ou horário estranho
Tudo isso pode ser implementado com plataformas como Sentry e DataDog.
Algumas lições reais
Para fechar, alguns incidentes reais de produção, do tipo que você prefere aprender lendo.
Caso 1: permissão de menu e permissão de rota inconsistentes
No menu, “relatórios financeiros” foi liberado para o papel de operações, mas a permissão da rota no middleware ficou sem essa regra. Resultado: o usuário via a entrada no menu, clicava e recebia 403. O feedback demorou uma semana para chegar.
Lição: gerencie permissões em um único lugar. Menu e rota devem usar a mesma configuração.
Caso 2: cache de permissões com atraso para entrar em vigor
Codificamos permissões no JWT com expiração de 24 horas. A equipe de operações revogou a permissão de um usuário, mas ele continuou acessando. Até o Token expirar no dia seguinte, o estrago já podia ter sido feito.
Lição: operações sensíveis não devem depender só do Token. Valide no backend consultando o banco, ou mantenha uma blacklist de permissões revogadas no Redis.
Caso 3: esquecimento de validação na API
O controle de permissões da página estava ótimo, mas uma API ficou sem validação. Alguém chamou direto pelo Postman e passou por todas as proteções do frontend.
Lição: a API do backend é a última barreira e precisa validar permissões. Use middleware ou decorators para padronizar; não dependa da memória de cada pessoa em cada endpoint.
Depois de tudo isso, a ideia central cabe em uma frase: permissão no frontend é experiência do usuário; permissão no backend é segurança. As duas importam, mas o backend importa mais.
Conclusão
Olhando para trás, sistema de permissões não é uma tecnologia misteriosa. Difícil é fazer bem.
Este texto foi do design de RBAC à implementação de middleware no Next.js 15, passando por menu dinâmico e componentes de tabela. O raciocínio central tem três partes:
- Não exagere no design: expanda conforme a necessidade; não comece com um modelo de permissões gigantesco
- Implemente em camadas: middleware intercepta rotas, menu é filtrado por permissão e botões aparecem conforme a ação permitida
- Use duas barreiras de segurança: frontend melhora a experiência, backend garante segurança; as duas precisam existir
Se você está desenvolvendo um painel administrativo, comece assim:
- Crie as quatro tabelas principais do RBAC: usuários, papéis, permissões e recursos
- Use middleware do Next.js para proteger rotas e extraia a configuração de permissões para constantes
- Implemente filtro dinâmico de menu e encapsule em um Hook reutilizável
- Use shadcn/ui + TanStack Table para tabelas, pois a flexibilidade é maior
Aquelas duas semanas refatorando permissões foram cansativas, mas valeram a pena. Hoje, para adicionar um novo papel, basta configurar no banco. Não preciso mudar uma linha de código. Quando o produto pediu um papel de “auditor”, resolvi em dez minutos.
Quando o sistema de permissões fica bem feito, a eficiência de desenvolvimento do time inteiro melhora. Não espere um incidente de segurança para levar isso a sério; aí já é tarde.
O projeto open source HaloLight, citado no texto, serve como referência: uma implementação completa com Next.js 15 + React 19 + TypeScript + RBAC. A qualidade do código é boa e vale estudar.
Por fim, se você encontrar problemas durante a implementação, pode deixar um comentário. Já passei por boa parte das armadilhas de sistemas de permissão e ajudo no que der.
Fluxo para implementar RBAC em um admin com Next.js
Passo a passo completo para criar do zero um sistema RBAC em um painel administrativo com Next.js.
⏱️ Estimated time: 120 min
- 1
Step 1: Passo 1: desenhe a estrutura das tabelas RBAC
Crie quatro tabelas principais e duas tabelas de associação:
**Tabelas principais**:
• User: dados básicos do usuário
• Role: definição de papéis, como admin, operator, viewer
• Permission: definição de permissões no formato resource:action, como user:create
• Resource, opcional: definição dos recursos
**Tabelas de associação**:
• UserRole: relação muitos-para-muitos entre usuário e papel
• RolePermission: relação muitos-para-muitos entre papel e permissão
**Padrão de nomenclatura**:
Use permissões no formato resource:action para facilitar gestão e busca.
**Pensando em expansão**:
Comece simples. Depois, se necessário, adicione tabelas Department e Position para suportar estrutura organizacional.
Use um ORM como Prisma para gerenciar a estrutura do banco e facilitar ajustes futuros. - 2
Step 2: Passo 2: implemente proteção de rotas com middleware do Next.js
Crie um arquivo middleware.ts na raiz do projeto:
**Configure o mapeamento de permissões por rota**:
• Crie uma constante ROUTE_PERMISSIONS
• Defina a lista de papéis exigida por cada rota
• Configure uma whitelist PUBLIC_ROUTES, como login e cadastro
**Lógica principal do middleware**:
1. Verifique se a rota é pública; se for, libere
2. Use getToken para obter a sessão do usuário
3. Redirecione usuários não logados para a tela de login, preservando a página de origem
4. Verifique se o papel do usuário atende à permissão exigida pela rota
5. Retorne uma página 403 quando a permissão for insuficiente
**Otimização de desempenho**:
• Codifique as permissões do usuário no JWT
• Evite consultar o banco em toda requisição
• Configure uma expiração razoável para o token, como 1 hora
**Configure o matcher**:
Exclua arquivos estáticos e rotas de API; valide apenas rotas de página. - 3
Step 3: Passo 3: implemente geração dinâmica de menu e filtro por permissões
Crie a configuração do menu e a lógica de filtragem:
**Estrutura de menu** (config/menu.ts):
• Use uma estrutura em árvore
• Cada item inclui key, label, icon, path e permission
• permission é opcional; sem ela, o item fica visível para qualquer usuário logado
**Algoritmo de filtragem** (lib/menu.ts):
• Implemente a função recursiva filterMenuByPermissions
• Trate a relação entre menus pai e filho: se o pai não tiver permissão, mas algum filho tiver, mostre o pai
• Retorne a árvore de menu filtrada
**Hook customizado** (hooks/usePermissionMenu.ts):
• Use useSession para obter as permissões do usuário
• Use useMemo para armazenar o resultado filtrado
• Evite recalcular quando as permissões não mudarem
**Destaque de rota e breadcrumbs**:
• Use usePathname para obter a rota atual
• Implemente getMenuPath para gerar o caminho do breadcrumb
• Dê suporte a parâmetros de rotas dinâmicas - 4
Step 4: Passo 4: integre shadcn/ui + TanStack Table
Implemente um componente de tabela reutilizável:
**Instale as dependências**:
• npx shadcn@latest add table
• npm install @tanstack/react-table
**Crie o componente DataTable**:
• Use o Hook useReactTable do TanStack Table
• Dê suporte a ordenação, paginação e filtros básicos
• Mantenha segurança de tipos com TypeScript
**Controle de permissões na tabela**:
• Permissão por coluna: use renderização condicional para mostrar colunas sensíveis
• Permissão por ação: encapsule um Hook usePermission para controlar botões
• Dê suporte a hasPermission, hasAnyPermission e hasAllPermissions
**Paginação no servidor**:
• A rota de API recebe page e size
• Use skip e take do Prisma para paginar
• Retorne a lista de dados e o total
**Validação de permissões**:
Controle de permissões na tabela do frontend é apenas otimização de UX. A API do backend precisa validar novamente. - 5
Step 5: Passo 5: valide permissões na API e em Server Actions
Garanta a barreira de segurança no backend:
**Validação em Server Actions**:
• No início de cada Server Action, chame auth() para obter a sessão
• Verifique papel e permissões do usuário
• Lance erro se a permissão for insuficiente
**Validação em rotas de API**:
• Use getToken para obter dados do usuário
• Valide a legitimidade da requisição
• Adicione uma segunda validação para operações sensíveis
**Configuração unificada entre frontend e backend**:
• Extraia a configuração de permissões para um módulo compartilhado
• Faça frontend e backend referenciarem a mesma configuração
• Em monorepo isso fica especialmente conveniente
**Registro de auditoria**:
• Registre operações críticas, como criar, excluir e alterar permissões
• Inclua operador, horário e conteúdo
• Logs devem ser append-only, sem alteração retroativa - 6
Step 6: Passo 6: otimize desempenho e reforce a segurança
Ajustes para produção:
**Otimização de desempenho**:
• Codifique permissões do usuário no JWT
• Use useMemo para cachear o menu filtrado
• Use code splitting por rota para reduzir o carregamento inicial
• Reduza verificações de permissão repetidas e desnecessárias
**Checklist de segurança**:
• Todas as APIs de backend validam permissões
• Há proteção contra privilege escalation
• O Token tem expiração razoável
• Frontend e backend validam entrada do usuário
• Zod define a estrutura dos dados
**Monitoramento e alertas**:
• Monitore o volume de erros 403
• Monitore requisições anormais de usuários
• Notifique em tempo real ações de superadministradores
• Envie e-mail quando a configuração de permissões mudar
**Armadilhas comuns**:
• Não valide permissões só no frontend
• Não deixe configuração de permissões hardcoded
• Mantenha permissões de menu e de rota consistentes
• Não dependa só do cache do Token para operações sensíveis
FAQ
Por que usar RBAC em vez de atribuir permissões diretamente aos usuários?
• **Gestão em lote**: ao adicionar 5 atendentes, basta atribuir o papel, sem configurar permissões 5 vezes
• **Atualização unificada**: ao alterar permissões de um papel, todos os usuários desse papel são atualizados na hora
• **Boa capacidade de expansão**: um usuário pode ter vários papéis, e as permissões são combinadas automaticamente
• **Menos erro operacional**: vínculos diretos usuário-permissão são fáceis de esquecer, criando riscos de segurança
Mais de 80% das aplicações SaaS empresariais no exterior usam RBAC porque ele equilibra flexibilidade e manutenibilidade.
Qual é a diferença entre middleware do Next.js e validação de permissão no componente?
**Middleware, recomendado**:
• Executa antes de a requisição chegar à página e intercepta de forma centralizada
• Tem bom desempenho, com resposta 60-80% mais rápida que validação no componente
• Mantém o código concentrado e reduz esquecimentos
• Suporta validação de permissões em renderização no servidor
**Validação no componente**:
• Só roda depois da renderização da página e pode causar piscada visual
• Precisa ser repetida em cada página e é fácil esquecer
• Cópia e manutenção custam caro
Mas lembre: o middleware é só a primeira barreira. A API do backend precisa validar permissões de novo.
Como tratar um menu pai sem permissão quando algum menu filho tem permissão?
**Lógica de exibição**:
• Se pelo menos um menu filho estiver visível, mostre o menu pai
• Assim o usuário consegue ver e acessar o submenu permitido
**Implementação**:
Use um algoritmo recursivo: primeiro filtre os filhos, depois decida se o pai deve aparecer.
1. Processe os submenus recursivamente
2. Verifique a permissão do item atual
3. Se o item atual não tiver permissão, mas tiver filhos visíveis, mantenha o item
4. Se não tiver permissão nem filhos visíveis, filtre o item
Essa abordagem mantém o controle de permissões rígido sem piorar a experiência do usuário.
Como escolher entre shadcn/ui + TanStack Table e Ant Design Table?
**Escolha Ant Design Table**:
• A equipe já conhece Ant Design
• Você precisa desenvolver rápido, com muita coisa pronta
• O painel administrativo segue um estilo empresarial tradicional
• O tamanho maior do bundle não é um problema
**Escolha shadcn/ui + TanStack Table**:
• O projeto usa Tailwind CSS
• Você precisa de alto controle visual
• Flexibilidade e desempenho são prioridade
• A equipe aceita investir mais tempo escrevendo código
**Comparativo de dados**:
A combinação shadcn/ui + TanStack Table cresceu mais de 300% entre 2024 e 2026 e virou uma escolha forte para admins modernos.
As duas opções são boas. A decisão depende das necessidades do projeto e da stack da equipe.
Como frontend e backend devem trabalhar juntos na validação de permissões?
**Validação no frontend** (middleware + componentes):
• Objetivo: melhorar a experiência do usuário e dar feedback rápido
• Onde fica: middleware intercepta rotas; componentes controlam exibição de botões
• Limite: pode ser contornada com ferramentas de desenvolvedor e não é barreira de segurança
**Validação no backend** (API + Server Actions):
• Objetivo: ser a barreira real de segurança
• Onde fica: cada Server Action e rota de API
• Obrigatório: toda operação sensível precisa validar; não dependa do frontend
**Configuração unificada**:
• Extraia a configuração de permissões para um módulo compartilhado
• Faça frontend e backend usarem a mesma configuração
• Garanta regras consistentes para evitar brechas
**Lição aprendida do jeito difícil**: em um projeto, o controle de permissões no frontend estava bom, mas uma API ficou sem validação. Alguém chamou direto pelo Postman e passou por todas as proteções.
As permissões devem ficar no JWT ou serem buscadas no banco em toda requisição?
**Opção 1: codificar no JWT, recomendada**:
• Vantagem: o middleware não precisa consultar o banco, então é rápido
• Desvantagem: depois de alterar permissões, é preciso esperar o Token expirar
• Serve para: cenários em que permissões mudam pouco
• Sugestão: expiração de Token em 1 hora
**Opção 2: cache em Redis**:
• Vantagem: melhor tempo real; a permissão entra em vigor imediatamente
• Desvantagem: adiciona uma dependência e mais complexidade
• Serve para: cenários em que permissões mudam com frequência
**Opção 3: consulta ao banco**:
• Vantagem: 100% em tempo real
• Desvantagem: consulta o banco em toda requisição e fica lento
• Não recomendo, exceto quando o negócio exigir
**Solução híbrida**:
JWT com permissões + blacklist no Redis para permissões revogadas. É um equilíbrio entre desempenho e atualização em tempo real.
Quais itens de segurança revisar antes de colocar o sistema de permissões em produção?
**Validação no backend**:
• Todas as Server Actions têm verificação de permissão
• Todas as rotas de API validam permissão
• Operações sensíveis têm segunda validação, como excluir usuário
**Proteção contra privilege escalation**:
• Usuário não pode alterar o próprio papel
• Usuário não pode adicionar permissões a si mesmo
• Usuário com pouca permissão não acessa recursos de alta permissão
**Auditoria e monitoramento**:
• Registre operações críticas em logs imutáveis
• Monitore o volume de erros 403
• Notifique ações de superadministradores em tempo real
• Envie e-mail quando permissões forem alteradas
**Gestão de sessão**:
• Token com expiração razoável, como 1 hora
• Suporte a logout forçado
• Token antigo invalida após troca de senha
**Validação de entrada**:
• Frontend e backend validam entrada
• Use Zod para definir estruturas de dados
• Evite SQL injection usando ORM como Prisma
Lembre: permissão no frontend é experiência do usuário; permissão no backend é barreira de segurança.
1 min de leitura · Publicado em: 7 jan 2026 · Atualizado em: 14 jul 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Guia completo de upload de arquivos no Next.js: S3 e Qiniu Cloud com URL preassinada
Aprenda a usar URLs preassinadas no Next.js para enviar arquivos direto ao S3 ou Qiniu Cloud, contornar o limite de 4 MB, aceitar uploads de até 5 GB e aplicar boas práticas de segurança, desempenho e produção.
Parte 38 de 51
Próximo
Como implantar Next.js na Vercel: variáveis de ambiente, domínio e monitoramento
Aprenda a implantar um projeto Next.js na Vercel, configurar variáveis de ambiente e domínio próprio, ativar SSL e acompanhar o desempenho sem cair nos erros mais comuns.
Parte 40 de 51





Comentários
Entre com GitHub para comentar