Todo estudio dice "construimos production-grade". La frase está tan gastada que ya no significa casi nada. Production-grade no es una vibra y no es un tier en una página de precios. Es una checklist con bordes afilados, y la mayoría de sus puntos no tienen nada que ver con el producto que el cliente está pagando.
Enviamos la misma checklist de una página con cada engagement. No es emocionante. No es lo que el fundador puso en el roadmap. Es la base aburrida que decide si el trabajo interesante encima sobrevive el contacto con usuarios reales. Este post es esa checklist, en forma de párrafos, con el razonamiento que normalmente saltamos en el kickoff porque nadie quiere oírlo.
Logs estructurados con un request ID, de punta a punta
Lo primero que añadimos a cualquier codebase heredado es logging estructurado con un request ID propagado desde el punto de entrada hasta la query de base de datos y cualquier job en segundo plano que se dispare por el camino. No "usamos un logger". No "tenemos una cuenta de Sentry". Una línea JSON por evento con un esquema estable, y un request ID que te deja reconstruir qué le pasó a un usuario específico en un momento específico.
La razón es mundana. A las 3 de la madrugada, con un reporte de bug vago de un solo cliente, tienes minutos —no horas— para encontrar la aguja correcta en el pajar. Logs grepables con un ID de correlación convierten esa búsqueda de una investigación de medio día en una query de dos minutos. El trabajo de añadir esto el día uno es pequeño. El trabajo de añadirlo retroactivamente, después de seis meses de print sin estructura, es un proyecto de varias semanas que nadie va a financiar.
Un store de secretos de verdad, no el .env de alguien
El número de sistemas en producción que hemos visto corriendo sobre un .env que vive en el portátil de un dev y se reenvía a mano cada vez que cambia es genuinamente deprimente. Eso no es un store de secretos. Es un rumor sobre un secreto.
Un store de secretos de verdad tiene control de acceso, logs de auditoría, rotación y una forma de revocar una credencial filtrada sin redeployar todos los servicios que la usan. La herramienta concreta importa menos que las propiedades. Elegimos lo que encaja con el destino de despliegue — managers cloud-native cuando ya estamos en la nube, un vault self-hosted cuando no. Lo no-negociable es que ningún secreto vive en un repositorio Git, ningún secreto se comparte por Slack, y la rotación es una operación rutinaria en vez de un pánico.
Auth que sobrevive el OWASP básico
La mayoría del código de autenticación que heredamos maneja el happy path. Un usuario con la contraseña correcta entra. Los unhappy paths son donde están enterrados los cadáveres. Invalidación de sesión cuando una contraseña cambia. Rate limits en login, reset de contraseña y cualquier endpoint que devuelva "sí, este email existe" en una diferencia medible de timing. Tokens CSRF acotados a las operaciones que realmente mutan estado, no esparcidos al azar por todos los formularios. Un modelo de permisos consistente en vez de if user.is_admin desperdigado por cuarenta vistas.
Nada de esto es investigación nueva. Cada punto está en la primera página del cheat sheet de OWASP. No estamos reclamando insight aquí — estamos reclamando disciplina. La checklist existe para que la disciplina no dependa de si el ingeniero que hizo el trabajo recordó cada categoría el día que escribió el formulario de login.
Schema con constraints reales
Un esquema de base de datos sin constraints es una sugerencia. Hemos perdido la cuenta de los codebases donde el equipo eventualmente descubre, por las malas, que user_id en alguna tabla es a veces NULL, a veces una referencia huérfana, y una vez —memorablemente— un string. El código de aplicación que depende de que estos datos se comporten razonablemente está lleno de chequeos defensivos que existen porque el esquema no está haciendo su trabajo.
Añadimos NOT NULL donde el valor es genuinamente requerido. Foreign keys donde la relación existe, con semántica ON DELETE explícita en vez de cualquier default que tenga Postgres. CHECK constraints para rangos y enums. Índices UNIQUE para cosas que son únicas. El coste es algo de incomodidad durante la migración. El beneficio es que toda una categoría de bugs "¿cómo llegamos a este estado?" se vuelve estructuralmente imposible.
Migraciones que corren hacia adelante Y hacia atrás
Una migración que solo corre hacia adelante es una puerta de un solo sentido. La envías, y desde ese momento tu capacidad para revertir la aplicación está acotada por tu capacidad para revertir la base de datos — que, si la migración fue destructiva, es cero.
Escribimos migraciones que tienen un path down definido, y lo testeamos. Para cambios destructivos —tirar una columna, estrechar un tipo— usamos el patrón expand-and-contract: enviar primero el cambio aditivo, deployar la aplicación que ya no necesita la columna vieja, y luego tirarla en una migración de seguimiento. Esto lleva más tiempo que el atajo. También significa que "revertir el deploy" sigue siendo una opción real durante toda la ventana en la que se podría detectar una regresión.
Deploys reversibles en menos de dos minutos
La propiedad más valiosa de un pipeline de despliegue es la capacidad de deshacerlo. No la capacidad de hacerlo. Cualquiera puede hacerlo. La pregunta es qué pasa cuando la release sale, la tasa de error se dispara, y alguien necesita hacer que el código nuevo deje de estar en vivo, ya mismo, antes de que la próxima demo de fin de sprint se convierta en una disculpa.
Un rollback de dos minutos no es una feature que enchufas después. Es una propiedad del pipeline entero: artefactos inmutables, switches atómicos entre versiones, migraciones de base de datos que no queman el puente detrás, secretos y configuración que no están enredados con la release. Diseñamos para esto desde el día uno porque retrofitear esto sobre un pipeline que no fue construido para ello es más difícil que construirlo bien la primera vez.
Runbooks de on-call legibles a las 3am
El último punto es el que más a menudo se salta, y el que paga el dividendo más grande la primera vez que ocurre un incidente real. Un runbook es un documento corto y romo que le dice al ingeniero de guardia qué hacer cuando una alerta específica salta. No "investigar el problema". Una lista de comandos, dashboards a revisar y las tres causas más probables ordenadas por frecuencia.
El test para un buen runbook es brutal: dáselo a un ingeniero que nunca ha visto este sistema, a las 3am, sin contexto, y mira si puede resolver la alerta sin paginar a nadie más. La mayoría de runbooks fallan este test en el primer intento. Está bien — el valor está en el segundo intento, después del incidente, cuando el equipo reescribe el runbook contra lo que realmente desearon haber sabido a las 3am. Tras unas iteraciones, el runbook se convierte en la memoria institucional que el equipo si no tendría que guardar entera dentro de cabezas humanas.
El trade-off, en voz alta
Esto es trabajo poco glamuroso. Lleva aproximadamente dos semanas de esfuerzo enfocado al inicio de un engagement, y envía cero features que el fundador pueda demostrarle a un cliente. Lo decimos claramente en cada kickoff, porque la alternativa es que el trabajo se salta silenciosamente y luego se re-litiga bajo presión de outage seis meses después, cuando el coste de hacerlo es tres veces mayor y la gente que lo hace no está durmiendo.
El pitch no es que esto sea divertido. El pitch es que la factura llega de cualquier modo. Puedes pagarla ahora, en condiciones tranquilas, con un equipo que tiene el contexto para hacerlo bien. O puedes pagarla luego, en un canal de incidente a medianoche, con quien sea que esté disponible. Recomendamos fuertemente la primera opción, y estructuramos nuestros engagements para que sea el default.
Y una nota sobre AI
Esta es también la barra que el código generado por AI no alcanza actualmente por sí solo. Un modelo te dará un draft plausible de cualquier punto de esta lista. El draft será aproximadamente 70% correcto y 100% silenciosamente equivocado sobre el 30% que importa — las cláusulas ON DELETE, la semántica fail-open del rate limit, el path de rollback que asume silenciosamente un deploy de instancia única. Cerrar esa brecha es exactamente el trabajo por el que nos pagan. Es, por el momento, el trabajo que todavía no se vibecodea.


