Caso de estudio 2: Una pequeña herramienta SaaS con autenticación y base de datos
La idea
Una diseñadora independiente quería un panel privado para registrar horas facturables por cliente y exportar un CSV mensual (Comma-Separated Values — valores separados por comas: un archivo de hoja de cálculo en texto plano que cualquier aplicación de hojas de cálculo puede abrir). Nada elaborado, pero necesitaba cuentas (para que sus datos fueran solo suyos) y persistencia.
La especificación
La autenticación y una elevan la apuesta, así que la especificación se volvió más específica sobre los límites:
A logged-in web app where a user can: sign up / log in with email,
create clients, log time entries (date, client, hours, note),
see a table of entries filtered by month, and download that month
as CSV. Each user only ever sees their own data. Mobile-friendly.
"Each user only ever sees their own data" (cada usuario solo ve sus propios datos) parece una línea de experiencia de usuario. En realidad es el modelo de seguridad de toda la aplicación, comprimido en nueve palabras. Nombrarlo en la especificación nos permitió señalarlo cada vez que la IA se desviaba.
La pila tecnológica
Para alguien que construye en solitario, la jugada ganadora es una pila donde la autenticación y la base de datos son servicios gestionados, no código que tú escribes. Elegimos una app de Next.js desplegada en un host sin servidor, con una base de datos Postgres alojada y un proveedor de autenticación listo para usar que gestiona por nosotros el inicio de por correo, las sesiones y los restablecimientos de contraseña. Menos código para equivocarse es menos código que una IA puede equivocar en nuestro nombre. La autenticación en particular es una categoría que casi nunca quieres construir a mano con una IA: los modos de fallo son silenciosos, el radio de impacto abarca la cuenta de todos, y un proveedor gestionado ha tenido sus casos límite puestos a prueba por millones de inicios de sesión.
Las piezas gestionadas quedan por fuera; la única regla que tú controlas queda en el medio: cada consulta filtra por user_id, de modo que un usuario nunca puede leer las filas de otro:
┌──────┐ ┌─────────────┐ ┌──────────────────┐
│ USER │─ login ─▶│ AUTH │─ user ─▶│ NEXT.JS APP │
└──────┘ │ (managed) │ id │ │
└─────────────┘ │ every query: │
│ WHERE user_id=? ─┼──┐
└──────────────────┘ │
▼
┌──────────────────┐
│ POSTGRES (managed)│
│ clients · entries │
└──────────────────┘
↑ managed = less to get wrong ↑ the one boundary YOU enforce
Los prompts clave
Dejamos que la propia plantilla del proveedor de autenticación hiciera el trabajo pesado, y luego dirigimos a la IA para que capa por capa añadiera la lógica de encima:
We're using [auth provider]'s Next.js starter. Add a Postgres
schema with two tables: clients (id, user_id, name) and
time_entries (id, user_id, client_id, date, hours, note). Every
query MUST filter by the logged-in user's id from the session.
Generate the migration and the typed data-access functions.
La línea "MUST filter by user_id" (debe filtrar por user_id) fue la frase más importante de todo el proyecto. Repetimos esa restricción en casi cada que tocaba datos, porque el bug más aterrador de una app multiusuario es que un usuario vea las filas de otro. Repetirla se siente redundante mientras la escribes; es exactamente esa redundancia la que te salva, porque el modelo no tiene memoria de cuánto importa esa restricción entre prompts separados.
Para la exportación:
Add a /api/export route that takes a month (YYYY-MM), pulls the
logged-in user's time_entries for that month joined to client
names, and streams a CSV download. Reject the request if there's
no valid session.
El obstáculo
Durante las pruebas, creamos dos cuentas y descubrimos que la cuenta B podía ver los clientes de la cuenta A en un menú desplegable. Este es exactamente el bug que temíamos. En lugar de pedirle a la IA que lo "arreglara", primero le hicimos demostrar el problema:
Account B is seeing Account A's clients. Show me every database
query in the codebase that reads the clients table, and for each
one tell me whether it filters by the session user_id. Don't fix
anything yet — just audit.
La auditoría reveló una consulta —la que cargaba el desplegable— que se había escrito antes de que añadiéramos la restricción y se había colado. Le pedimos que agregara el filtro que faltaba, y luego pedimos una salvaguarda:
Add a single helper that every read goes through, which takes the
session and injects the user_id filter, so no future query can
forget it. Refactor the existing queries to use it.
Eso convirtió una corrección puntual en una garantía estructural. Después hicimos que esa garantía fuera comprobable, porque una salvaguarda que no puedes verificar es solo una esperanza:
Write a test that creates two users, has each create a client,
then asserts that user A's session can never read user B's client
through any of the data-access functions.
La lección: cuando una IA introduce un fallo de seguridad, no te limites a parchar el caso puntual; dirígela para que elimine la categoría del error, y luego cierra esa categoría con llave mediante una prueba que falle ruidosamente si alguien vuelve a abrirla.
El lanzamiento
Sembramos un mes de datos de prueba, exportamos el CSV, lo abrimos en una hoja de cálculo para confirmar que los números y la codificación de caracteres eran correctos, y luego pusimos una contraseña fuerte a la base de datos y rotamos las credenciales fuera de cualquier archivo local. Desplegamos en el host sin servidor, añadimos las variables de entorno de producción en su panel, y le dimos la . Ella misma se registró con un alta real. Toda la construcción tomó un fin de semana.