Alternar tema

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

Easton editorial illustration: one admin console with a central role gate and three protected modules

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.

80%+
Aplicações SaaS empresariais usam RBAC
Flexibilidade e manutenibilidade ficam em equilíbrio: mais simples que ABAC e mais flexível que vínculo direto usuário-permissã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/users só 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ário
  • order:delete - excluir pedido
  • report: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:

  1. É preciso repetir em toda página, uma cópia sem fim
  2. É fácil esquecer uma página e abrir uma brecha
  3. A página renderiza antes da validação, então o usuário pode ver uma piscada
  4. 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.

60-80%
Melhora no tempo de resposta
A validação de permissões por middleware é 60-80% mais rápida que a validação em componente, pois reduz renderizações desnecessárias

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.
300%+
Crescimento do shadcn/ui
Entre 2024 e 2026, a combinação shadcn/ui + TanStack Table cresceu mais de 300% e virou uma das principais escolhas para admins modernos

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:

  1. Não exagere no design: expanda conforme a necessidade; não comece com um modelo de permissões gigantesco
  2. Implemente em camadas: middleware intercepta rotas, menu é filtrado por permissão e botões aparecem conforme a ação permitida
  3. 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. 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. 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. 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. 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. 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. 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?
A principal vantagem do RBAC é o custo de manutenção:

• **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?
Eles têm papéis diferentes:

**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?
A estratégia recomendada é priorizar o filho:

**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 de acordo com o projeto:

**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?
Use duas camadas, cada uma com sua função:

**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?
O melhor é decidir por cenário:

**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?
Use um checklist completo:

**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

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog