El día que el login funcionaba pero nadie podía entrar
Cinco horas de debugging, una cookie cross-subdomain mal configurada, y la lección de por qué los tests E2E no son opcionales en una arquitectura multi-tenant.
Eran las once de la mañana del martes cuando el primer mensaje llegó por Slack. "Hola, no puedo entrar a mi blog. La página de login dice OK pero me devuelve a login otra vez."
Cinco minutos después, un segundo mensaje. Otro usuario, mismo síntoma.
Esto es lo que pasó, lo que tardamos en encontrar la causa, y lo que cambiamos para que no vuelva a pasar.
Síntoma: 200 OK, sin sesión
El flujo de login en Agentikas es estándar: el usuario va a write.agentikas.ai/login, mete email y password, la API valida con Supabase, y al cerrar el ciclo el usuario aterriza en write.agentikas.ai/dashboard ya autenticado.
El bug era sutil. La request a /api/auth/login devolvía 200 OK. La cookie sb-access-token aparecía en los Set-Cookie headers de la respuesta. El cliente la procesaba sin error. Y al hacer la siguiente request — la que cargaba /dashboard — el servidor no veía la cookie, devolvía 401, y el middleware redirigía a login.
Loop infinito. Pero loops infinitos sin error. La consola del navegador, limpia. Los logs del servidor, perfectos. Nada decía que algo iba mal — excepto que el usuario, claramente, no podía entrar.
El callejón sin salida del primer instinto
El primer instinto fue revisar la configuración de Supabase. Las claves estaban bien. La validación funcionaba — devolvía un JWT correcto. El JWT tenía las claims correctas. Si copiabas el access token de los headers y lo metías a mano en la cookie del navegador, el dashboard cargaba.
Una hora gastada en eso. Conclusión: el JWT está bien, el problema está en la cookie en sí.
Segundo instinto: revisar Set-Cookie. Los headers parecían correctos:
Set-Cookie: sb-access-token=eyJhbG...; Path=/; HttpOnly; Secure; SameSite=Lax
Path correcto. HttpOnly correcto. Secure correcto (estamos en HTTPS). SameSite Lax — el default de las cookies modernas, que debería funcionar para navegación normal.
Otra hora gastada. Conclusión: la cookie llega, los flags parecen correctos.
El momento "ah"
El detalle que se nos había escapado: la API y el dashboard no están en el mismo subdominio.
El dashboard sirve desde write.agentikas.ai. La API de auth, durante un refactor reciente que nadie había revisado bien, se movió a api.agentikas.ai/auth/login. La cookie se setea en respuesta de api.agentikas.ai, así que su Domain implícito es ese. Cuando el navegador hace la siguiente request a write.agentikas.ai, no manda la cookie — porque no es del mismo dominio.
Pero aquí viene el truco: write.agentikas.ai y api.agentikas.ai son subdominios del mismo apex (agentikas.ai). La solución es simple: setear la cookie con Domain=.agentikas.ai para que sea válida en cualquier subdominio.
El refactor original había omitido el domain option en cookies.set(). El default del SDK de Supabase es no setearlo, asumiendo same-origin. En desarrollo local funcionaba porque todo corría en localhost. En producción, api. y write. son orígenes distintos.
El fix
Cinco líneas de código. La opción correcta en el cliente de Supabase server-side:
cookies.set({
name: "sb-access-token",
value: token,
options: {
domain: ".agentikas.ai", // ← la línea que faltaba
path: "/",
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7,
},
});
Deploy. Test manual. Funciona. Cinco horas para descubrir que faltaba una línea.
Lo que cambiamos para que no vuelva
El fix de cinco líneas no es lo importante. Lo importante es lo que añadimos para que un bug así no llegue a producción otra vez.
1. Test E2E del flujo completo. Un test de Playwright que va a write.agentikas.ai/login, mete credenciales, navega a /dashboard, y verifica que el usuario está autenticado. Antes existía un test que verificaba la API devolvía 200. Ese test pasaba con el bug. Ahora el test prueba el comportamiento, no la implementación.
test("login flow ends with authenticated dashboard", async ({ page }) => {
await page.goto("https://write.agentikas.ai/login");
await page.fill('input[name="email"]', "test@example.com");
await page.fill('input[name="password"]', process.env.TEST_PWD!);
await page.click('button[type="submit"]');
await page.waitForURL("**/dashboard");
await expect(page.getByRole("heading", { name: /tu blog/i })).toBeVisible();
});
2. Lint rule custom para cookies. Un lint que falla si cookies.set() en server-side se llama sin el option domain en archivos bajo apps/dashboard/src/app/api/. Suena overkill — y lo es para un proyecto pequeño — pero el coste de añadirlo es bajo y elimina la categoría entera de bug.
3. Documentar el dominio de cookies en CLAUDE.md. Cuando Claude (o cualquier dev nuevo) añada una nueva ruta de auth, va a leer el CLAUDE.md del proyecto. La nota dice: "Las cookies de auth siempre deben ser domain: .agentikas.ai para funcionar cross-subdomain. Local dev usa el default del SDK; prod requiere el override explícito."
La lección, sin epifanía
Hay una tentación al cerrar un post-mortem: extraer una lección grandilocuente. "Aprendimos que la observabilidad es crítica." Eso es siempre verdad y casi siempre inútil.
La lección real es más prosaica: los tests que verifican comportamiento end-to-end capturan bugs que los tests de implementación no. Cuando tu test dice "la API devuelve 200", no estás probando que el usuario puede entrar. Estás probando que el handler corre. Son cosas distintas.
Y cuando tu arquitectura tiene subdominios — y casi todas las modernas lo tienen — la cookie cross-domain es exactamente el tipo de detalle que se rompe en producción y funciona en local. No por magia, por física.
Agentikas es open source. El test que añadimos vive en github.com/agentikas/agentikas-blog bajo tests/e2e/auth.spec.ts. El commit del fix tiene un mensaje honesto — recomendado leerlo si te toca alguna vez auth con cookies cross-subdomain.
Comentarios
Cargando comentarios…
Inicia sesión en tu dashboard para participar.