Ir al contenido
LUINIniciar un proyecto

Playbook

Endurece una app vibe-coded sin tirar la velocidad

El demo funcionó. Ese es el problema. Cómo falla el código generado con IA cuando llegan usuarios, atacantes y facturas, y en qué orden endurecerlo.

Por LUINIngeniería de IA · Seguridad

La lista de facturas cargó a la primera. Mandaste el link. Para el jueves un cliente ya tenía las facturas de todas las demás organizaciones, porque la ruta tomaba orgId del query string y nadie preguntó quién tenía permitido mandar uno. Esa es la forma del problema. El producto funciona. “Funciona” se midió contra un demo: un usuario, una organización, un happy path, sin atacante, sin factura, sin las tres de la mañana. A eso le dicen vibe coding. Te sientas con un modelo, describes la siguiente pantalla, aceptas el archivo y sigues. El loop es conversación, no review. Es una buena forma de enterarte si el producto debería existir. Es una mala forma de decidir si el producto merece confianza. El código no es el error. Publicarlo como si alguien lo hubiera leído sí lo es.

Las fallas son concretas

El código generado con IA no falla al azar. Falla en los lugares que el demo nunca visitó.

Secretos que se pasean

El modelo necesita que el programa corra. El camino más corto es una key en el archivo, o una variable de entorno que no es secreta porque va prefijada para el browser.

const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY!);

NEXT_PUBLIC_ no es una decisión de estilo. Todo lo que lleva ese prefijo se va en el bundle del cliente. El modelo vio “variable de entorno” y se detuvo. Rota la key. Luego haz una búsqueda en el historial, porque borrar la línea no borra el commit. La misma forma aparece como un .env commiteado, un JSON de service account dejado en /secrets “nada más para local”, y una API key pegada en un prompt que después se imprimió sola en un toast de error.

Autorización en el happy path

export async function GET(req: Request) {
  const session = await getSession(req);
  if (!session) {
    return new Response("Unauthorized", { status: 401 });
  }
  const orgId = new URL(req.url).searchParams.get("orgId");
  const invoices = await db.invoice.findMany({ where: { orgId } });
  return Response.json(invoices);
}

Esto está bien si le crees al query string. No deberías. El check de sesión pasó. La fila igual salió del orgId que mandó quien llamó. La autenticación responde quién eres. La autorización responde qué puedes tocar. La IA es notablemente buena para autenticar y pésima para autorizar. Recorre cada ruta que lee o escribe una fila. Si el único check es “hay sesión”, esa ruta es pública para cualquier usuario logueado. Una referencia insegura a un objeto no es un ataque ingenioso. Es el default.

Errores que se ven como éxito

try {
  await chargeCustomer(order);
} catch (err) {
  console.log(err);
  return { ok: true };
}

La UI muestra una confirmación. Finanzas no. El modelo aprendió que una excepción sin atrapar hace que el demo se vea roto, así que aprendió a cachar todo y seguir. Producción pide lo contrario: falla a la vista, guarda el id, niégate a mentir.

Migraciones que solo sirven en la laptop

ALTER TABLE invoices ADD COLUMN amount_cents integer;
UPDATE invoices SET amount_cents = amount * 100;
ALTER TABLE invoices DROP COLUMN amount;

En la laptop esto es instantáneo. En una tabla grande de producción el UPDATE toma un lock, el DROP borra la única columna a la que podrías haber hecho rollback, y el deploy ya siguió de largo. Primero expande, haz el backfill por lotes, cambia las lecturas, tira la columna después. Un statement que hace las tres cosas es un demo de valor, no una migración. El resto de la lista es más callado y igual de caro. Un N+1 que estaba bien con diez filas y se vuelve hostil a los diez mil; un loop sin tope porque el fixture tenía tres ítems; tests generados para afirmar que la función regresa lo que la función regresa. Rangos de dependencias sin pin y sin review, que es como un paquete transitivo se vuelve el incidente. Sin request id en los logs, un deploy que es “merge to main”, y una pregunta de licencia que nadie le hizo a un paquete que ahora se sienta en el path de pagos. Nada de esto es exótico. Es lo que obtienes cuando el acceptance test es “se pintó”.

Endurecer es un orden, no un estado de ánimo

Un rewrite se siente limpio porque esconde cada decisión que no tomaste. También esconde el único path que ya cobra dinero. Quédate con el producto. Cambia el contrato que tiene con el mundo. El orden importa porque cada paso produce información que el siguiente necesita. Observabilidad antes de un refactor, o pules la función equivocada. Tests después de saber cómo se ve un incidente, o compras cobertura y te comes el bug. El perímetro de seguridad antes de la limpieza visual, o publicas un hoyo más bonito.

Siete pasos numerados en una línea; una flecha de cada uno al siguiente.
El orden para endurecer. Cada paso produce lo que el siguiente necesita.
  1. Inventaría lo que existe

    Lista las superficies que corren: rutas, jobs, colas, webhooks, páginas de admin, scripts que todavía tienen credenciales de producción, esa Cloud Function que nadie recuerda. Anota dónde vive el estado y quién puede tocarlo. Una búsqueda en el repo por process.env, sk_live, BEGIN PRIVATE KEY y TODO es un arranque, no el mapa. No puedes endurecer un sistema que no puedes nombrar.

  2. Encuentra las tres cosas que sí te pueden doler

    No treinta hallazgos. Tres. Qué pierde datos, dinero o confianza si está mal esta semana: el path de cobro, el export, el endpoint que toma un id del cliente. Todo lo demás espera. Una lista larga es cómo el endurecimiento se convierte en un rewrite con otro nombre.

  3. Arregla primero el perímetro de seguridad

    Pon la autorización junto a los datos, no en el componente que casualmente los pinta. Saca el tenant de la sesión, no del query string. Saca los secretos del bundle y de git. Rota todo lo que alguna vez se haya commiteado. Cierra la ruta de admin que el modelo dejó en el mismo origin sin un check extra. Haz esto antes de renombrar folders. Un codebase ordenado con la lista de facturas abierta sigue siendo una lista de facturas abierta.

  4. Pon observabilidad antes de mejorar nada

    Necesitas un request id, logs estructurados y un error que le llegue a una persona. Necesitas saber si corrió el cargo, no si cargó la página. Agrega la sonda que te habría avisado de la fuga de tenants del jueves: quién pidió qué orgId, y si coincidía con la sesión. Si no puedes ver una falla, te vas a pasar la siguiente semana “mejorando” una función que no era el problema.

  5. Escribe las pruebas que habrían cachado el incidente

    La cobertura es una métrica de vanidad cuando la suite afirma que el código hace lo que el código hace. Escribe el caso que habría fallado el jueves: el usuario A pide las facturas del usuario B y se le niega. Escribe el caso donde chargeCustomer lanza y la respuesta no es { ok: true }. Una prueba que nombra una falla real vale más que un archivo generado que no nombra ninguna.

  6. Haz que el deploy tenga rollback

    Un migrate-on-boot que solo va para adelante no es un proceso de release. Necesitas un deploy que puedas deshacer, una migración que puedas expandir o echar para atrás, y un flag que apague un path nuevo sin rebuild. Si el único rollback es “git revert y ojalá el schema esté de acuerdo”, no tienes rollback. Tienes un deseo.

  7. Entonces, y solo entonces, refactoriza

    Ahora sí puedes renombrar el módulo, partir el archivo que el modelo dejó en novecientas líneas y borrar el cliente que el scaffold creó y nadie usa. Hazlo detrás de los tests y los logs que acabas de poner. Refactorizar primero es cómo un equipo se gasta un mes haciendo más elegante la fuga.

Trata cada patch del modelo como el cambio de un contratista que no conoces: lee el diff, corre el path que puede perder dinero y rechaza cualquier cosa cuya historia de autorización sea “en la UI no se ve ese botón”.

Con qué te quedas

La velocidad era el punto. No castigues el loop que te consiguió un producto. Cambia lo que significa “listo”. Listo no es “la página se pintó para mí”. Listo es: la ruta rechaza a quien no debe, la falla se ve, el deploy regresa, y la prueba que habría cachado el jueves existe antes del feature del viernes. El modelo puede seguir escribiendo el primer draft de la ruta. Una persona sigue decidiendo si esa ruta puede existir. Un equipo que hace esto bien no se ve más lento por la tarde. Se ve aburrido. Alguien pega un prompt. Alguien lee el diff. Los secretos se quedan en un gestor de secretos, no en el bundle del cliente. La autorización es un gate en el handler, no un comentario en el ticket. Los logs llevan un request id. El deploy tiene un botón que lo deshace. Cuando el modelo se inventa una dependencia, una persona abre la página del registry antes de que cambie el lockfile. Siguen publicando un martes. No se enteran el jueves de que orgId era una sugerencia. El endpoint de facturas puede quedarse. Ya nada más no le cree al query string.

Práctica relacionada

Ingeniería de IA

¿Te sirvió?

Solicita una sesión informativaCotiza en línea