No boilerplate que uso para começar projetos, o commit que integra o primeiro provedor de auth e o que o substitui pelo segundo estão separados por dois dias. Dois dias. O primeiro colocava middleware para rotas protegidas e uma tela de login; o segundo arrancava tudo e punha outro SDK com suas próprias páginas de sign-in e sign-up. Desde então esse mesmo template passou por mais dois provedores e terminou, quase um ano depois, com um middleware escrito à mão.
Não é azar. É um padrão, e já o repeti em pelo menos quatro repositórios.
Sempre começo com um provedor gerenciado, e sempre pelo mesmo motivo
No dia um de um projeto eu não quero escrever hash de senhas, verificação de e-mail, nem o fluxo de OAuth com o Google. Instalo um SDK, coloco duas variáveis de ambiente, e numa tarde há login. É a decisão correta naquele dia: o projeto ainda não sabe se vai existir daqui a um mês.
O problema é que esse SDK não fica quieto na borda da aplicação. Se enfia no layout, no contexto de usuário, em cada server action que precisa saber quem está chamando, e no esquema de banco de dados porque o user_id da tabela dele é o que acaba sendo chave estrangeira em todas as suas.
As quatro migrações e o que disparou cada uma
- Provedor um para provedor dois, em dois dias. O primeiro funcionava, mas o modelo de preços e o acoplamento com o dashboard dele não encaixavam. Nenhuma linha de auth sobreviveu à troca.
- Provedor dois, seis meses de convivência. Aqui teve de tudo: notas no README sobre seu comportamento estranho com Suspense, refactors do middleware para ler os domínios protegidos de um arquivo de config, e uma mudança de
useProfileparauseUserpara parar de manter minha própria camada em cima da deles. - Provedor dois para provedor três. Um PR inteiro chamado "migrate from" mais o nome do anterior. Trinta arquivos tocados. Apagam-se
stack.tse a rota catch-allapp/handler/[...stack]/page.tsx, e aparecemauth.tsno cliente eapp/api/auth/[...path]/route.tsno servidor. - Provedor três para middleware próprio. O commit diz literalmente "replace neonAuth with custom auth middleware and update session handling". Do SDK gerenciado fico com o cliente; o resto eu controlo.
O que sempre quebra: as URLs
Na segunda migração há dois commits no mesmo dia, um atrás do outro. O primeiro deixa o arquivo de configuração assim:
export const stackServerApp = new StackServerApp({
tokenStore: "nextjs-cookie",
urls: {
signIn: "/ass",
signUp: "/asdds",
},
});
Isso é o que acontece quando você está testando na mão para onde cada coisa redireciona e esquece a digitação de teste ali. O segundo commit devolve os dois para /auth. É ridículo e é exatamente a falha que você vai engolir em toda migração de auth: o provedor não sabe onde mora sua tela de login, então há uma URL declarada na config dele que precisa bater com uma rota real do router, e não há tipos que verifiquem isso. Redireciona para um 404 e o sintoma não é "URL errada": é "o login não funciona".
A outra metade do que quebra é a forma da sessão. Na passagem para o middleware próprio, getSession deixou de devolver o objeto do SDK e passou a devolver algo explícito:
type SessionData = NonNullable<Awaited<ReturnType<typeof auth.getSession>>["data"]>;
export type Session = SessionData | { session: null; user: null };
Esse | { session: null; user: null } é a concessão que é preciso fazer. O SDK anterior devolvia um nullable pelado e metade do código fazia session?.user. Ao normalizar, todos os lugares que assumiam que a sessão podia ser undefined pararam de compilar. Essa é a parte cara da migração, não o login.
O que ganho e o que perco escrevendo eu mesmo
O que ganho é concreto: controle sobre o objeto de sessão, sobre onde o cookie é guardado e sobre o que acontece exatamente quando não há sessão. E uma coisa que eu não tinha valorizado até precisar: poder colocar regras de autorização no mesmo lugar.
Numa ferramenta de BI para uma empresa grande, a autorização acabou sendo uma lista de e-mails no próprio middleware:
const authorizedEmails = [ /* ... */ ];
const hasFullAccess = userEmail && authorizedEmails.includes(userEmail);
Com dois redirecionamentos distintos: o não autenticado vai para o login, o autenticado sem permissão vai para a home. Isso, com um provedor gerenciado, se faz brigando com as claims ou o painel dele. Ali escrevi em dez linhas. Também deixei um console.log("User email:", userEmail) que teve que sair num commit posterior, e naquele app a auth foi reescrita três vezes em um mês.
O que perco é tudo o que o provedor fazia e você não via: rotação de tokens, invalidação de sessões, o e-mail de recuperação, o rate limit do login. Nada disso é impossível, mas passa a ser código meu, sem testes que eu mesmo não tenha escrito.
Meu critério agora
Não é "escreva sua própria auth". É mais chato: assuma desde o dia um que você vai trocar de provedor. O que faço hoje antes de instalar qualquer coisa:
- Uma única função
getSession()no meu código. Nenhum componente importa o SDK. O tipo de retorno eu defino, não infiro do provedor. É a diferença entre migrar trinta arquivos e migrar um. - O
user_iddo meu banco de dados é meu. O do provedor vai numa coluna à parte. Senão, a migração deixa de ser uma troca de biblioteca e vira uma migração de dados. - As URLs de login numa constante, não na config do provedor. Que o
/assnão tenha onde se esconder. - Autorização nunca dentro do provedor. Quem pode entrar é do provedor; o que pode fazer é do meu código.
Faz pouco tempo comecei um projeto novo e voltei a começar com um provedor gerenciado. No primeiro dia não deu para entrar: a URL base de auth apontava para um valor de preenchimento do boilerplate. O ciclo começa de novo, e desta vez sei exatamente onde vai doer.
