Un comando rutinario de migración borró toda la base de datos de producción

Un desarrollador que se autodescribe como programador por intuición concedió a Claude Opus 5, ejecutándose en el modo ultracode de Claude Code, acceso de escritura a una base de datos de Supabase en producción. El agente ejecutó entonces un comando de migración de apariencia normal: prisma migrate diff con el parámetro shadow-database-url apuntando a la variable de entorno DATABASE_URL_UNPOOLED. Ese único comando eliminó todas las tablas de la base de datos de producción. La herramienta migrate diff de Prisma utiliza una base de datos sombra, es decir, una base de datos desechable que construye desde cero para calcular la diferencia entre el esquema actual y un estado objetivo. Para construir esa base de datos desechable, Prisma la reinicia primero: elimina todo su contenido y luego reproduce el historial de migraciones en la base de datos vacía.

La base de datos sombra es desechable por diseño, y el parámetro shadow-database-url indica a Prisma dónde se encuentra esa base de datos desechable. En este caso, DATABASE_URL_UNPOOLED apuntaba a la base de datos de producción. Supabase proporciona dos cadenas de conexión: una con agrupación de conexiones para el tráfico de la aplicación, y otra directa que la propia documentación de Supabase recomienda usar para migraciones, porque el agrupador no puede ejecutar comandos DDL de forma limpia. El desarrollador tenía la URL directa de producción en el entorno. El agente tomó la cadena de conexión que tenía disponible y ejecutó un reinicio sobre ella. Así, lo que debía ser un reinicio de la base de datos sombra se convirtió en la eliminación y reconstrucción de la base de datos de producción. La reconstrucción reprodujo la carpeta local de migraciones, que estaba desactualizada. Todas las tablas fueron eliminadas. El agente lo comunicó sin rodeos: "La base de datos ha sido borrada. Es culpa mía y debo informarte de inmediato."

Las 22 tablas desaparecieron, incluidas dos que nunca se recuperaron

Las 22 tablas quedaron vacías: 130 herramientas, 21 comparaciones, usuarios, reseñas y valoraciones. Dos tablas, BlogPost y ApiKey, nunca habían estado en la carpeta de migraciones, por lo que fueron eliminadas y nunca recreadas. El esquema volvió de forma parcial. La reconstrucción solo restauró lo que las migraciones habían registrado. Cualquier tabla que existiera en producción pero que no estuviera rastreada en la carpeta de migraciones, ya fuera porque se creó con SQL directo, a través del panel web de Supabase o por un compañero que omitió la herramienta de migración, fue eliminada y permaneció eliminada. Así fue como BlogPost y ApiKey desaparecieron por completo: eran tablas reales con datos reales, pero no tenían archivo de migración. La reproducción no tenía registro de ellas, así que nunca las restauró. Una reconstrucción considerada exitosa dejó igualmente el esquema sin las tablas de las que dependía la aplicación.

Dos fallos tuvieron que acumularse para que ocurriera el desastre

Gran parte de la reacción en línea encuadró el incidente como un caso de inteligencia artificial descontrolada. No fue así. El agente ejecutó exactamente el comando que ejecutaría un desarrollador durante una migración normal, y el comando hizo lo que está diseñado para hacer. Dos fallos independientes tuvieron que acumularse para que se produjera el borrado. Por un lado, al agente se le proporcionaron credenciales de producción y la autorización para usarlas: el modo ultracode es un modo de amplia autonomía de escritura cuyo objetivo es reducir la fricción, pero esa baja fricción apuntada a una base de datos de producción es el peligro, no una ventaja. El agente no podría haber borrado nada a lo que no tuviera acceso.

Por otro lado, el comando tenía una trampa oculta: el parámetro shadow-database-url no es de solo lectura, ya que Prisma reinicia la base de datos en esa URL antes de reproducir las migraciones. Si se apunta a cualquier base de datos que no se pueda permitir perder, Prisma la borrará por diseño. Si se elimina cualquiera de los dos factores, el borrado no ocurre. El agente no puede borrar una base de datos a la que no tiene credenciales. Una URL de base de datos sombra apuntada a una base de datos desechable segura reinicia esa base de datos desechable, no la de producción.

Por qué la carpeta de migraciones desactualizada agravó el problema

El reinicio elimina todas las tablas, pero la reconstrucción solo vuelve a crear lo que las migraciones registraron. La lección es más antigua que la inteligencia artificial: la carpeta de migraciones es la fuente de verdad del esquema. Si una tabla existe en producción pero no en la carpeta, el esquema ya está roto, y la próxima migración que reconstruya desde la carpeta lo pondrá de manifiesto eliminándola. Tratar el esquema que existe fuera de la carpeta de migraciones como un error, y no como un atajo, es una práctica fundamental de gestión de bases de datos que este incidente ilustra de forma dolorosa.

Por qué culpar a la IA es el enfoque equivocado

Si la narrativa es que el modelo se descontroló, la solución sería esperar a un modelo más inteligente. Esa solución no arregla nada, porque el modelo no se descontroló. Ejecutó un comando documentado con un comportamiento documentado contra una base de datos a la que se le entregaron las claves. Culpar al modelo deja intactos los dos fallos reales: las credenciales de producción en el entorno del agente y la URL de la base de datos sombra apuntando a producción. El agente incluso actuó con honestidad después del incidente: se detuvo e informó del daño de inmediato, en lenguaje claro. El fallo no estuvo en el juicio del modelo, sino en el radio de daño potencial que el ser humano configuró antes de que al modelo se le pidiera hacer nada.

Este incidente tiene la misma forma que todos los demás incidentes de filtración de credenciales de producción de los últimos veinte años: un script de integración continua con claves de producción incluidas en el repositorio, un portátil con una clave SSH de producción, un comando DROP DATABASE ejecutado contra la variable DATABASE_URL equivocada. El agente es una nueva forma de desencadenar un fallo antiguo. Las defensas son las defensas de siempre: mantener las credenciales de producción fuera del radio de daño y asumir que cualquier herramienta con capacidad de acción acabará usándolas.

Las medidas de protección que habrían evitado el incidente

Nunca se debe apuntar una base de datos sombra a producción. La opción shadowDatabaseUrl debe configurarse en el archivo schema.prisma apuntando a una base de datos dedicada y desechable, y dejar de pasar shadow-database-url en la línea de comandos. La base de datos sombra es desechable por definición; si una URL puede alcanzar producción, no es una base de datos sombra. Las credenciales de producción deben mantenerse fuera del entorno del agente: el agente ejecutó este comando porque DATABASE_URL_UNPOOLED estaba disponible en su shell. Un shell sin credenciales para el agente, o un rol de base de datos de solo lectura para exploración, elimina la capacidad de hacer esto. Los comandos destructivos deben requerir aprobación humana: Prisma dispone del modo create-only, que escribe un archivo de migración sin aplicarlo, y de modos de solo diferencia. Una política que bloquee migrate deploy, migrate reset y cualquier migrate diff con shadow-database-url contra producción a menos que un humano lo apruebe convierte un borrado de un solo comando en una revisión de un solo comando.

La carpeta de migraciones debe estar completa: cada tabla que exista en producción debe tener una migración que la cree. Las tablas BlogPost y ApiKey fueron irrecuperables porque nadie escribió nunca migraciones para ellas. Finalmente, deben existir copias de seguridad con recuperación en un punto en el tiempo: Supabase ofrece esta funcionalidad. Un borrado es recuperable en minutos desde una copia de seguridad; es catastrófico sin ella. El radio de daño de un error lo determina la posición de recuperación, no si el error provino de un humano o de un agente.