NextAuth v5 (que ahora se llama Auth.js para ser más amigable) es una de esas piezas de software donde la documentación oficial te lleva el 80%, y el último 20% lo descubres a las patadas en producción. Esta es la receta que me funciona en ReclamaAI con Google OAuth + email/password coexistiendo, y los detalles que ojalá hubiera sabido al principio.
El setup base
Mi auth.ts exporta tres cosas: handlers (los route handlers POST/GET para el endpoint de auth), signIn/signOut (server actions para usar desde server components), y auth (helper para leer la session en cualquier server component). Es la API más limpia que ha tenido NextAuth.
Los providers configurados son dos: Google (con scope mínimo: profile y email) y Credentials (email + password con bcrypt). El adapter es el oficial de Prisma. La estrategia de session es "jwt", no database — más rápido para reads frecuentes y suficientemente seguro con rotación.
El gotcha #1: account linking automático
Imagina este flujo: María se registra con email/password (maria@gmail.com) en marzo. En mayo vuelve, no recuerda que se registró, y hace clic en "Iniciar sesión con Google" usando la misma cuenta de Gmail. NextAuth v5 por default rechaza el login con un error OAuthAccountNotLinked. Para el usuario, eso se ve como "no me deja entrar y no entiendo por qué".
La solución NO es activar allowDangerousEmailAccountLinking: true en el provider de Google. Eso es inseguro porque cualquier persona que registre un Gmail puede enlazarse a una cuenta existente de email/password. La solución correcta es:
- Detectar el error
OAuthAccountNotLinkeden la página de login. - Mostrar al usuario: "ya tienes una cuenta con este correo. Inicia sesión con tu contraseña, y desde tu perfil podrás vincular Google".
- En la página de perfil, ofrecer un botón "vincular Google" que requiere que el usuario ya esté autenticado por contraseña primero.
Más fricción, sí. Pero seguro y predecible.
El gotcha #2: session callbacks y datos extra
La session por default contiene solo user.id, user.email, user.name. Si quieres que session.user.role o session.user.credits estén disponibles en cada request, tienes que ponerlos manualmente en el callback session.
Lo que aprendí a las patadas: el callback session se ejecuta en cada request que llama auth(). Si haces queries pesadas ahí, tu app se vuelve lenta sin que sepas por qué. Mi regla: el callback session solo lee del JWT (que ya está en memoria). Los datos extra los pongo en el JWT desde el callback jwt, y solo cuando hay un evento de login o de update — no en cada read.
El gotcha #3: la session no se actualiza sola
Si un usuario compra créditos y quiero que su session.user.credits refleje el nuevo total inmediatamente, NO va a pasar mágicamente. La session vive en una cookie firmada, y solo se regenera cuando el usuario hace login de nuevo o cuando llamas update() explícitamente desde el cliente.
La solución: después de cualquier acción del backend que cambie datos relevantes para la UI (compra, cambio de plan, cambio de role), el cliente llama useSession().update() para forzar la regeneración. Esto trigger un nuevo callback jwt con el flag trigger: "update", donde puedes recargar los datos de la base.
El gotcha #4: middleware vs server components
En Next.js 15+, el middleware corre en edge runtime (Edge functions). NextAuth v5 puede leer la session ahí, pero si tu callback jwt hace cualquier cosa que use Node-only APIs (Prisma, bcrypt), el middleware se rompe en edge.
La solución oficial: separar el config en dos archivos. auth.config.ts tiene solo lo que es edge-safe (los providers básicos, los callbacks que no tocan DB, las páginas custom). auth.ts importa el config y agrega el adapter de Prisma y los callbacks pesados, y se usa solo desde server components y route handlers, NO desde el middleware. Es feo pero funciona.
Por qué no Clerk
Clerk es genuinamente más fácil. Si estás empezando un producto y no tienes restricciones de costo o de control, úsalo y no mires atrás. Yo no lo uso por dos razones: el costo escala rápido cuando llegas a usuarios activos (free tier es generoso pero el segundo escalón sube rápido), y porque mover usuarios fuera de Clerk si después decides irte es trabajo. Con NextAuth + Prisma, mis usuarios viven en mi base de datos, son míos, y si mañana quiero cambiar de proveedor de auth solo cambio el adapter.
Lo que vale la pena hacer desde el día uno
- Logging detallado en los callbacks
signIn,jwt,session. Cuando algo se rompe en producción, vas a agradecer poder ver qué evento disparó qué callback. - Email verification para credentials. Si alguien se registra con email/password, manda un correo de confirmación y bloquea el login hasta que verifique. Si no lo haces, vas a tener cuentas fantasma y problemas de spam de registro.
- Rate limiting en el endpoint de login con credentials. NextAuth no lo hace por ti.
En total, el primer setup me tomó 3 horas. Los gotchas me tomaron otras 6 horas a lo largo de las primeras semanas. Vale completamente la pena vs implementar auth a mano.