Cuando alguien estima un SaaS nuevo, el login suele ser el primer punto de la lista y el más barato. Dos campos de texto y un botón. He escuchado esa estimación en voz alta muchas veces, incluso de mi propia boca.

El problema no es la dificultad. Es el largo de la lista.

La lista que nadie escribe en la estimación

Registro e inicio de sesión son las dos pantallas que imaginaste. Después viene el correo de confirmación, y con él el usuario que nunca lo recibió, que necesita un botón para reenviarlo, que solo tiene sentido si sabes distinguir "contraseña incorrecta" de "cuenta todavía sin confirmar". Son mensajes distintos y caminos distintos.

Recuperar la contraseña es otro flujo completo: la pantalla que pide el correo, el enlace con token, la página que lee ese token de la URL, la validación, la pantalla de contraseña nueva y qué pasa cuando el enlace ya venció. En CastorStack el token de recuperación llega en el fragmento de la URL y se lee antes de que se renderice cualquier cosa, porque un fragmento sobrevive mal a una redirección.

Después está la sesión. Dónde vive, cuánto dura, qué pasa cuando el access token expira con el usuario a mitad de un formulario ya lleno. Refrescar en silencio o cerrarle la sesión en la cara son decisiones de producto disfrazadas de decisiones técnicas.

Nada de esto es difícil. Súmalo todo y tienes dos semanas, no una tarde.

La mitad que no tiene pantalla

El frontend solo entrega un token. Alguien tiene que creerle.

La API de CastorStack valida los JWT de Supabase pidiendo las claves públicas al JWKS del propio proyecto, con rotación de claves contemplada y un minuto de tolerancia a la diferencia de reloj entre máquinas. Se verifican emisor, audiencia y vigencia. Existe un camino HS256 para desarrollo local, el tipo de atajo que debe estar aislado y documentado, porque es justo lo que se filtra a producción cuando nadie está mirando.

Esa parte no aparece en ninguna captura del producto y es lo único que impide que alguien falsifique un token y entre como otra persona.

Saber quién eres no es saber qué puedes hacer

La autenticación responde "quién eres". Queda la pregunta que el producto hace de verdad: puedes ver esta página, llamar a este endpoint, descargar este archivo.

Son ejes distintos. El usuario está autenticado, pero su suscripción venció ayer. Está autenticado y pagando, pero su plan no incluye la función. Está autenticado, pagando y en el plan correcto, pero apagaste la función para todos después de un incidente.

En CastorStack eso se resuelve con atributos en los endpoints y una evaluación de acceso que consulta el plan, las funciones del plan y un interruptor de apagado por función. El panel administrativo es una aplicación aparte, con despliegue aparte, y todas sus rutas viven bajo un prefijo propio protegido por el rol de administrador. La separación física cuesta poco y elimina una clase entera de errores.

El agujero que Supabase deja abierto por defecto

Este se lleva su propia sección porque casi se nos pasa aquí.

Supabase publica cada tabla de la base de datos mediante una API REST automática, accesible con la clave anónima. Esa clave está en el bundle del frontend, donde cualquiera puede leerla. Es a propósito: la protección real es el Row Level Security de Postgres, y no se activa solo en las tablas que tu API crea por migración.

La corrección fue una migración que activa RLS en las catorce tablas gestionadas por el servidor, revoca los permisos de los roles públicos y cambia el privilegio por defecto del esquema, para que cualquier tabla creada de ahí en adelante nazca cerrada. Esa última parte es la que importa: la protección tiene que ser el valor por defecto, no un punto de checklist que alguien va a olvidar en la próxima migración.

Errores en tres idiomas

Si el producto vende fuera de tu país, "contraseña inválida" también tiene que existir fuera de tu idioma. Las respuestas de error de la API siguen la RFC 7807 y se traducen según la cabecera de idioma de la petición, en los mismos tres idiomas que el resto del sistema.

Detalle pequeño, pero es de los que nadie vuelve a arreglar. Se queda en inglés para siempre.

Lo que no está aquí

No hay autenticación de dos factores. No hay bloqueo de cuenta tras varios intentos fallidos, ni política de contraseñas escrita en este repositorio: esas opciones viven en el panel de Supabase, y llamarlas función de la plantilla sería mentira. El login social no está implementado. No existe listado ni revocación de sesiones activas. No hay organizaciones ni equipos, y eso fue una decisión, no un olvido: la unidad de alcance es el perfil, y la multi-tenencia es de las cosas que se agregan cuando un cliente la pide, no antes.

El registro de auditoría guarda lo que ocurre en la API, así que los cambios de catálogo, de proveedor de pago y de contenido quedan grabados con autor, IP y qué cambió. El login y el logout no aparecen ahí, porque ocurren en Supabase y nunca tocan la API.

Prefiero esa lista escrita a descubrirla en la semana del lanzamiento.

Por qué esto importa más ahora

Escribir el CRUD se volvió barato. Un modelo genera la pantalla de login en segundos, y hasta funciona.

Lo que no genera es la decisión de dónde guardar el refresh token, el RLS activado antes de que alguien apunte un cliente REST a tu base de datos, o el recuerdo de que reenviar la confirmación es un flujo y no un botón. Esas cosas se aprenden una vez, y sale más barato aprenderlas en el código de otro.