Todos los recursos

Cambiar el esquema a base de clics en un panel funciona perfecto, hasta el día en que necesitas un segundo entorno, un compañero de equipo o poder revertir sin sustos. Esta guía convierte tu esquema de Postgres en archivos SQL ordenados que viven en git y se aplican igual en todas partes (local, CI y producción) con la CLI de Supabase. Sales de aquí con un flujo de migraciones funcionando, un control de CI que falla si hay cambios fuera de control y una regla clara de qué puedes tocar y qué no.

Versiona tu esquema de Postgres con la CLI de Supabase

En resumen

  • Las migraciones convierten tu esquema en un artefacto reproducible en git: archivos SQL con marca de tiempo en supabase/migrations que se aplican en orden e igual en cada entorno.
  • La CLI te da un ciclo real de local a producción: escribes el cambio, lo pruebas contra un Postgres local en Docker y después lo subes al proyecto remoto.
  • La regla que te mantiene a salvo: nunca edites una migración que ya corrió en producción. Escribe una migración nueva hacia adelante.
  • Un control de CI (supabase db diff --linked) detecta la desincronización del esquema que se cuela cuando alguien hace un cambio a mano en el panel.
  • Los datos de carga inicial, las políticas RLS y los cambios destructivos piden cada uno su propia disciplina. Ahí es donde se quema la mayoría de los equipos.

Hacer cambios a base de clics en el panel de una base de datos es rápido y da sensación de productividad, y funciona perfecto hasta el día en que necesitas un segundo entorno, un compañero de equipo o una respuesta honesta a "¿qué esquema hay exactamente en producción ahora mismo?". En ese punto el panel se vuelve un problema: no hay historial, no hay revisión y no hay forma de reproducir ese estado en otro lado. Las migraciones resuelven esto convirtiendo el esquema en un artefacto versionado: archivos SQL ordenados en git que se aplican igual en tu computadora, en CI y en producción. Esta guía recorre el flujo exacto que uso en proyectos como Infuse y la CME Platform: escribes un cambio, lo pruebas contra un Postgres local, lo subes y mantienes CI honesto para que nadie te mueva producción por debajo sin avisar.

Esto es un cómo y un por qué al mismo tiempo. Vas a tener comandos y SQL listos para copiar y pegar, pero también te voy a decir cuál es el paso que sostiene todo y dónde se esconden las fallas silenciosas, porque los errores que duelen en las migraciones nunca son de sintaxis: son errores de historial.

01 · Requisitos y el modelo mental

Antes de tocar un solo comando, deja claro el modelo mental, porque explica cada decisión que viene después. Una migración es una instrucción hacia adelante: "desde el estado actual, haz esto". La CLI guarda una carpeta con estas instrucciones, cada una nombrada con una marca de tiempo para que se ordenen cronológicamente, y una tablita de seguimiento en la base de datos que lleva la cuenta de cuáles ya corrieron. Aplicar migraciones significa "corre, en orden, todos los archivos que la base de datos todavía no ha visto". Esa es toda la idea, y casi todas las trampas que vienen más adelante salen de ahí.

Necesitas tener unas cuantas cosas listas primero:

  1. La CLI de Supabase instalada. Mejor una versión fijada por proyecto (con npx supabase o como dependencia de desarrollo) que una instalación global, para que cada compañero y cada corrida de CI usen la misma CLI.
  2. Docker corriendo en local. La CLI levanta un Postgres real en un contenedor para las pruebas locales. No es una imitación; es el mismo motor que producción.
  3. Un proyecto de Supabase que sea tuyo, y su project ref (la cadena corta que aparece en la URL del proyecto).
  4. Tus cambios viviendo en un repo de git. Las migraciones solo rinden cuando se revisan y se integran como cualquier otro código.

Importante

Define tu "fuente de la verdad" desde el día uno: es la carpeta de migraciones en git, no el panel. En el momento en que el panel pasa a ser el lugar donde se hacen los cambios, las migraciones empiezan a mentir, y cada entorno aguas abajo hereda esa mentira. Si un compañero mete un cambio a mano, el arreglo es capturarlo como migración (sección 05), no seguir haciendo clics.

Inicializa la estructura del proyecto una sola vez, desde la raíz de tu repo:

supabase init

Esto crea un directorio supabase/ con una carpeta migrations/ y un config.toml. Hazle commit. Esa carpeta es, de ahora en adelante, la descripción canónica de tu esquema.

02 · Enlaza el proyecto y levanta el Postgres local

Enlazar amarra tu repo local con el proyecto remoto para que push y diff sepan a dónde apuntar. Esto lo haces una sola vez por máquina:

supabase link --project-ref tu-project-ref

Te va a pedir la contraseña de la base de datos, la del rol de Postgres, no la de tu cuenta de Supabase. Ahora arranca el stack local:

supabase start

La primera corrida descarga las imágenes de Docker y tarda un minuto o dos; de ahí en adelante es rápida. Cuando termina, la CLI imprime una API URL local, una DB URL y el Studio corriendo en un puerto local. Ya tienes un Supabase completo corriendo en tu computadora (Postgres, Auth, la capa REST) que puedes romper con total libertad sin tocar nada real.

Nota

Mantén dos terminales en la cabeza, no dos bases de datos en las manos. Local es para iterar; remoto es para el resultado de una migración ya revisada. El único puente entre ambos es db push, y vale la pena desconfiar un poco cada vez que lo cruzas.

03 · Crea y escribe una migración

Generar una migración crea un archivo SQL vacío con marca de tiempo dentro de supabase/migrations/:

supabase migration new add_notes_table

Te queda un archivo como 20260328153000_add_notes_table.sql. El prefijo con la marca de tiempo es lo que garantiza el orden, así que nunca lo renombres. Escribe el cambio adentro como SQL puro. Mantén un solo asunto por migración (una tabla por aquí, un índice por allá) para que la revisión sea sencilla y, si algo falla, sepas de inmediato dónde mirar.

create table notes (
  id bigint generated always as identity primary key,
  user_id uuid references auth.users not null,
  body text not null,
  created_at timestamptz default now()
);

alter table notes enable row level security;

create policy "users read own notes"
  on notes for select
  using (auth.uid() = user_id);

"Solo hacia adelante" es una ventaja, no una limitación

Vas a notar que aquí no hay un archivo aparte de "down" o de reversión, y es a propósito. Para un equipo pequeño, las migraciones que solo van hacia adelante son más simples y seguras que mantener pares reversibles que nadie prueba. Si necesitas deshacer algo, escribes una migración nueva que lo elimina o lo altera. Tu historial solo crece y tu relato se mantiene honesto. El problema de las migraciones "down" es que se pudren: la mitad de bajada casi nunca se ejecuta, así que casi nunca funciona el día que de verdad la necesitas a las 2 de la madrugada.

Dónde va el RLS

Trata el row-level security como parte del esquema, no como algo que dejas para después en el panel. La política de arriba viaja en la misma migración que la tabla que protege, así que la tabla nunca queda revisable, y mucho menos desplegable, sin sus reglas de acceso. Es la misma disciplina con la que Infuse mantiene los secretos bien acotados: la postura de seguridad es código en el repositorio, no una casilla que alguien se puede olvidar de marcar.

04 · Prueba en local y después haz push al remoto

Aplica tus migraciones pendientes a la base de datos local y confirma que el cambio se comporta como esperas:

supabase migration up

Esto corre solo los archivos que el Postgres local todavía no ha visto. Abre el Studio, inserta una fila, prueba una query como usuario autenticado y asegúrate de que la política RLS haga lo que debe. Cuando lo veas sólido, aplica esas mismas migraciones a tu proyecto remoto:

supabase db push

db push envía, en orden, cada migración que le falta a la tabla de seguimiento remota y las registra. Como tanto local como prod aplican exactamente los mismos archivos en exactamente el mismo orden, terminan en el mismo esquema. Y esa convergencia es toda la ganancia del enfoque.

Un ejemplo concreto

Digamos que le vas a agregar etiquetas a la app de notas. Corres supabase migration new add_tags, escribes en ese único archivo un create table tags más una tabla de unión y sus políticas RLS, y después supabase migration up para aplicarlo en local. En el Studio creas un par de etiquetas, las enganchas a una nota y confirmas que otro usuario no las puede ver. Convencido, abres un PR que lleva tanto el archivo de migración como el código de la aplicación que lee las etiquetas, juntos, en una sola unidad revisable. Tras la integración, CI corre db push contra producción y las tablas nuevas aparecen, ya protegidas, sin un solo clic y sin sorpresas. El siguiente entorno que levantes hereda el mismo esquema gratis, con solo volver a reproducir la carpeta.

Consejo

Engancha db push a CI cuando se integra a tu rama principal, en vez de correrlo desde tu computadora. Que una persona haga push a mano es justo el momento en que los entornos empiezan a divergir en silencio: a alguien se le olvida, o lo corre desde una rama vieja. Deja que el pipeline sea lo único que toca el esquema de producción.

05 · Mantén producción honesta: desincronización, datos de carga inicial y CI

La falla que más golpea a los equipos es la desincronización: el esquema de producción ya no coincide con la carpeta de migraciones, casi siempre porque alguien metió un cambio a mano en el panel en medio de un incidente. Apenas eso pasa, db push queda trabajando sobre una premisa falsa. Detéctalo temprano con un diff:

supabase db diff --linked

Esto compara el esquema remoto en vivo contra tus migraciones e imprime la diferencia en SQL. Si la salida sale vacía, estás en sincronía. Cualquier otra cosa es desincronización, y tienes que capturarla como una migración nueva (pega el diff en un archivo recién creado con migration new, revísalo e intégralo) para que la carpeta se ponga al día con la realidad. Corre este mismo comando en CI como un control que rompe la compilación cuando el diff no está vacío. Eso convierte "nos desincronizamos hace tres semanas y nadie se dio cuenta" en "el PR está en rojo".

Para los datos de carga inicial, las filas de referencia que tu app necesita para funcionar, como los planes de suscripción o los códigos de país, usa supabase/seed.sql, que la CLI aplica cada vez que haces un reset local. Mantenlo idempotente (con on conflict do nothing o insert ... where not exists) para que volver a correrlo nunca tire un error. No metas datos reales de usuarios ni nada secreto en estos archivos; se versionan en git como cualquier otro.

Atención

Nunca edites una migración que ya se aplicó en producción. La tabla de seguimiento tiene ese archivo marcado como ejecutado, así que editar su contenido no hace nada en los entornos que ya lo corrieron, pero un entorno nuevo sí va a reproducir tu versión editada. Local y producción terminan divergiendo con el mismo nombre de archivo, que es el tipo de error más difícil de encontrar. Para cambiar algo que ya enviaste, siempre escribe una migración nueva hacia adelante que lo altere o lo elimine.

Los cambios destructivos piden cuidado extra. Un drop column o un drop table pierde datos en el instante en que corre, y db push lo va a ejecutar sin la menor ceremonia. Para cualquier cosa destructiva, opta por un proceso de dos pasos, expandir y luego contraer: primero envía una migración que agrega la forma nueva y rellena los datos, despliega el código de la app que la usa, y solo en una migración posterior eliminas la columna vieja, una vez que ya nada la lee. En una tabla grande, un alter table descuidado también puede tomar un bloqueo largo y trancar las escrituras; para esos casos, échale mano a un índice creado de forma concurrente o a un relleno por lotes en lugar de una sola sentencia bloqueante.

Las migraciones no tienen nada de glamour, y justo por eso valen la pena: convierten los cambios de esquema de un acto nervioso, manual y sin documentar en código aburrido, revisable y repetible. Si aciertas con la regla de la fuente de la verdad, nunca reescribes historial ya aplicado y dejas que CI vigile la desincronización, tu base de datos deja de ser la parte que da miedo de un despliegue. El día que levantes un segundo entorno y simplemente funcione es el día en que toda esa disciplina se paga sola.

Puntos clave

  • La carpeta de migraciones en git es la fuente de la verdad, no el panel. En cuanto el panel pasa a ser el lugar donde se hacen los cambios, cada entorno hereda la mentira.
  • El ciclo es escribir, supabase migration up (local) y luego supabase db push (remoto); los dos aplican los archivos pendientes en orden de marca de tiempo para que los entornos converjan.
  • Nunca edites una migración ya aplicada; escribe una migración nueva hacia adelante. Reescribir el historial es justo como local y producción terminan divergiendo en silencio con el mismo nombre de archivo.
  • Corre supabase db diff --linked en CI para detectar la desincronización, y envía las políticas RLS en la misma migración que la tabla que protegen.
  • Maneja los cambios destructivos y los de tablas grandes con expandir-y-contraer y con un ojo en los bloqueos, no con una sola sentencia bloqueante.

Preguntas frecuentes

¿De verdad necesito migraciones para un proyecto pequeñito?

Si de verdad es algo de usar y tirar, no. Hacer clics en el panel está bien para un prototipo de fin de semana. La línea se cruza en el primer momento en que otra persona, otro entorno o tu yo del futuro necesita reproducir el esquema. Las migraciones casi no cuestan nada al arrancar (supabase init más un archivo de migración) y te salvan el día que no puedes responder '¿qué hay en producción?'. Agregarlas después, ya con la desincronización encima, duele muchísimo más que empezar con ellas desde el principio.

¿Cuál es la diferencia entre db push y migration up?

Los dos aplican migraciones pendientes, pero a destinos distintos. supabase migration up corre los archivos que tu Postgres local todavía no ha visto; es tu ciclo de iteración. supabase db push corre los archivos que le faltan al proyecto remoto y los registra en la tabla de seguimiento remota. La regla mental: migration up es para probar en local, db push es para llevar un cambio ya revisado al remoto. Puedes estar relajado corriendo migration up todo el día, y un poco más atento cada vez que corres db push.

Alguien cambió el esquema en el panel. ¿Cómo lo recupero?

Corre supabase db diff --linked. Compara el esquema remoto en vivo contra tu carpeta de migraciones e imprime la diferencia en SQL; ese diff es justamente el cambio que se hizo a mano. Crea una migración nueva con supabase migration new, pega el diff adentro, revísalo como cualquier otro cambio e intégralo. Listo: la carpeta vuelve a reflejar la realidad y los entornos futuros la van a reproducir. No trates de 'deshacer' el cambio del panel; captúralo hacia adelante.

¿Por qué no mantener migraciones down/rollback?

Porque la mitad de bajada casi nunca se prueba, así que casi nunca funciona cuando por fin la necesitas bajo presión. El historial que solo va hacia adelante solo crece y es honesto: para revertir un cambio escribes una migración nueva que lo elimina o lo altera, y esa migración nueva pasa por la misma revisión y la misma prueba local que todo lo demás. Los pares reversibles tienen sentido para algunos equipos grandes con herramientas estrictas, pero en la mayoría de los proyectos solo agregan mantenimiento y una falsa sensación de seguridad, sin dar nada a cambio.

¿Cómo manejo los datos de carga inicial aparte de las migraciones?

Mantenlos separados. Las migraciones describen el esquema (tablas, columnas, políticas); los datos de carga inicial insertan filas de referencia que tu app necesita para arrancar, como los planes de suscripción o los códigos de catálogo. Ponlos en supabase/seed.sql, que la CLI aplica cada vez que haces un reset local, y mantenlos idempotentes con on conflict do nothing para que volver a correrlos nunca falle. Nunca metas datos reales de usuarios ni secretos en este archivo; se versiona en git como cualquier otro. Si una fila es de verdad parte del significado del esquema, también la puedes insertar dentro de una migración, pero la mayoría de los datos de referencia van en la carga inicial.

¿Cómo elimino una columna de forma segura en una tabla en vivo?

Hazlo en dos pasos, no en uno. db push va a correr un drop column en el mismo instante en que se integra, y esos datos se van de inmediato. Usa expandir y luego contraer: primero envía una migración que agrega la forma nueva y la rellena, despliega el código de la app que deja de leer la columna vieja, y solo en una migración posterior, cuando ya nada la referencia, eliminas la columna vieja. En una tabla grande, cuídate también de los bloqueos: un alter table descuidado puede bloquear las escrituras, así que mejor un índice creado de forma concurrente o un relleno por lotes antes que una sola sentencia bloqueante.

¿Prefieres que lo hagamos por ti?

Esto mismo lo construimos para negocios como el tuyo. La primera conversación es gratis y sin compromiso.

Escríbenos por WhatsApp

Escríbenos por WhatsApp

Escanéalo con tu teléfono para escribirnos por WhatsApp.

Escanéalo con tu teléfono para escribirnos por WhatsApp.

¿Estás desde el teléfono y no puedes escanear? Escríbenos a info@ilustrari.com

Primera conversación gratis. Te responde el fundador.

Recursos relacionados