Por que o acesso de cada rota é um dado, e não código
Todo app multi-tenant chega no mesmo ponto: alguém cria uma página nova e esquece de chamar o guard. O detalhe cruel é que isso não produz sintoma nenhum. A página abre, os dados carregam, o teste manual passa — porque quem testou estava logado como dono da organização.
O bug só aparece quando a pessoa errada abre a URL certa. Normalmente meses depois, e normalmente pelo suporte.
A decisão vira dado
Em vez de espalhar a checagem por dezenas de arquivos, este template concentra a decisão em um mapa:
export const ROUTE_ACCESS = {
"/precos": PUBLIC,
"/painel": ANY_MEMBER,
"/configuracoes/faturamento": OWNER_ONLY,
} as const satisfies Record<string, Access>;
Não existe default. Uma rota sem entrada no mapa não "cai no seguro" — ela impede o build.
As quatro travas
- Um script no
prebuildvarre a árvore de rotas e falha nas duas direções: rota sem
decisão, e decisão sem rota.
- Um teste roda a mesma varredura, para quem executa a suíte sem buildar.
- Um teste de matriz executa cada par (papel × rota) e congela o resultado inteiro.
Afrouxar uma rota sem querer quebra o teste.
- Um teste de navegação reconcilia o menu com o mapa.
Item de menu para rota negada é armadilha de UX. Rota permitida fora do menu é funcionalidade invisível. As duas divergências nascem da mesma causa: duas listas escritas à mão que precisavam concordar.
O que isso custa
Uma linha por rota, escrita no mesmo commit da página. É pouco, e é o ponto: o custo de lembrar precisa ser menor que o custo de descobrir.
Quer ver o mapa inteiro? Ele vive em apps/web/lib/route-access.ts, e o comentário no topo dele explica cada uma das travas.