
Migrar de Firebase a Supabase: una app real, de punta a punta (de Firestore a Postgres, Auth, Storage y RLS)
Usa el repo supabase-community/firebase-to-supabase: collections.js lista tus colecciones, firestore2json.js exporta cada una a JSON y json2supabase.js la importa a Postgres. El trabajo real es el salto de NoSQL a relacional: los objetos anidados caen como jsonb, así que desnormaliza en tablas relacionadas con llaves foráneas antes de importar. Luego migra Auth (conserva los hashes scrypt, sin reset), Storage y reescribe las Security Rules como RLS.
Por qué los equipos dejan Firebase por Supabase (y qué tiene que cambiar de verdad)
La respuesta corta: usa el repo supabase-community/firebase-to-supabase. Con collections.js listas tus colecciones de Firestore, con firestore2json.js exportas cada una a un archivo JSON, y con json2supabase.js la importas a una tabla de Postgres. Esa es la mecánica. El trabajo real —lo que ninguna guía en español te cuenta de punta a punta— es el salto de NoSQL a relacional: los documentos anidados de Firestore caen como columnas jsonb por defecto, así que tienes que desnormalizar en tablas relacionadas con llaves foráneas antes de importar. Después migras Auth (conservando los hashes scrypt, sin reset de contraseña), Storage, y reescribes las Security Rules como políticas RLS.
Los equipos se mueven por tres razones concretas: costo predecible, tener Postgres de verdad (SQL, JOIN, constraints, transacciones) en vez de un document store, y un solo sistema donde base de datos, auth, storage y RLS viven juntos. Pero que quede claro qué es fácil y qué es difícil: la herramienta funciona bien. Lo difícil es el modelo de datos. Firestore te dejó anidar todo dentro de un documento; Postgres quiere filas normalizadas y relaciones explícitas.
Firestore (NoSQL)
- Colecciones de documentos
- Objetos y arreglos anidados dentro del doc
- Sin esquema fijo
- Security Rules propias
- Sin JOIN
Supabase (Postgres)
- Tablas con columnas tipadas
- Datos normalizados en tablas relacionadas
- Esquema explícito
- RLS con auth.uid()
- JOIN, constraints, transacciones
Prepara el repo de migración: firebase-service.json + supabase-service.json
Clona el repo de la comunidad. Trae cuatro directorios —firestore/, auth/, storage/ y functions/— y se configura con dos archivos de credenciales de tipo service account.
git clone https://github.com/supabase-community/firebase-to-supabase.git
cd firebase-to-supabase
Necesitas dos archivos en la raíz del proyecto:
firebase-service.json— las credenciales del service account de Firebase (Firebase Console, Project Settings, Service accounts, Generate new private key).supabase-service.json— las credenciales de tu proyecto de Supabase.
{
"host": "db.xxxxxxxxxxxx.supabase.co",
"password": "your-database-password",
"user": "postgres",
"database": "postgres",
"port": 5432
}
Con eso, los scripts de cada directorio ya saben leer de Firebase y escribir en Postgres. Una advertencia importante: el repo evoluciona, así que verifica los nombres de los scripts y la ubicación de los parámetros de hash contra el repo vivo antes de correr nada. Los que uso aquí son los documentados al momento de escribir.
Exporta Firestore: collections.js y luego firestore2json.js
Primero, lista qué colecciones tienes. Este script recorre tu Firestore y te devuelve los nombres.
node collections.js
Después exportas cada colección a un archivo JSON local, una por una. firestore2json.js toma el nombre de la colección y escribe el archivo automáticamente como <colección>.json; los dos argumentos opcionales son el tamaño de lote y un límite de filas.
node firestore2json.js <nombre_de_la_coleccion>
Corre esto para cada colección que quieras migrar: users, orders, products, lo que tengas. Por ejemplo, node firestore2json.js users genera users.json. El resultado es un JSON por colección. Todavía no lo importes: aquí es donde entra la parte de ingeniería de verdad.
- collections.jsLista tus colecciones de Firestore
- firestore2json.jsExporta cada colección a un JSON local
- Hook o transform propio (HOOKS.md)Reshape: desnormaliza los objetos anidados
- json2supabase.jsImporta el JSON a una tabla de Postgres
El salto de modelo: de colección a tabla, de objeto anidado a jsonb
Cuando json2supabase.js importa, “aplana” la colección en una tabla de Postgres. Las propiedades simples se convierten en columnas tipadas: text, numeric o boolean según el valor. Y aquí está el detalle que te va a morder si no lo previenes: cualquier sub-objeto o arreglo anidado aterriza como una columna jsonb.
Eso a veces es lo que quieres —un blob de metadata que solo lees completo está perfecto en jsonb—. Pero cuando lo anidado es data relacional de verdad (los pedidos de un usuario, las líneas de un pedido), dejarlo en jsonb es arrastrar el pecado original de NoSQL a Postgres. Pierdes las llaves foráneas, los JOIN eficientes y la capacidad de aplicar RLS por fila hija. La regla práctica: si vas a consultar, filtrar o proteger esos datos por separado, no los dejes como jsonb. Desnormalízalos.
Desnormaliza antes de importar: un arreglo de orders embebido a tablas users + orders
El caso clásico. En Firestore tienes una colección users donde cada documento trae un arreglo orders embebido:
{
"id": "user_123",
"email": "ana@example.com",
"name": "Ana",
"orders": [
{ "orderId": "o_1", "total": 480, "status": "paid" },
{ "orderId": "o_2", "total": 120, "status": "pending" }
]
}
Si importas esto tal cual, orders queda como una columna jsonb dentro de users. Lo que quieres son dos tablas relacionadas: users y orders, unidas por una llave foránea user_id. El mecanismo soportado por el repo es un custom hook (documentado en HOOKS.md) —o tu propio script de transformación— que separa el arreglo embebido en su propio archivo con la referencia al padre.
// transforma users.json (con orders embebidos) en users_flat + orders_flat
import fs from "node:fs";
const users = JSON.parse(fs.readFileSync("users.json", "utf8"));
const usersFlat: any[] = [];
const ordersFlat: any[] = [];
for (const u of users) {
const { orders = [], ...rest } = u;
usersFlat.push(rest); // sin el arreglo anidado
for (const o of orders) {
ordersFlat.push({ ...o, user_id: u.id }); // llave foránea al padre
}
}
fs.writeFileSync("users_flat.json", JSON.stringify(usersFlat, null, 2));
fs.writeFileSync("orders_flat.json", JSON.stringify(ordersFlat, null, 2));
Ahora tienes dos JSON limpios: uno para users sin el arreglo, y otro para orders donde cada fila apunta a su usuario. Eso es normalizar: convertir documentos anidados en filas relacionadas con llaves foráneas. Es el paso que separa una migración que aprovecha Postgres de una que solo mudó el NoSQL de casa.
Importa a Postgres con json2supabase.js
Con los JSON ya normalizados, importa cada uno a su tabla. json2supabase.js crea la tabla infiriendo tipos de columna a partir del JSON.
node json2supabase.js users_flat.json
node json2supabase.js orders_flat.json
Después de importar, formaliza la relación en Postgres: agrega la llave primaria, la llave foránea y los índices que Firestore nunca te pidió declarar. Esto es lo que te da integridad referencial y JOIN rápidos.
alter table orders
add constraint orders_user_id_fkey
foreign key (user_id) references users (id)
on delete cascade;
create index orders_user_id_idx on orders (user_id);
Verifica los tipos que infirió el importador. Si una columna que debía ser numeric quedó como text, corrígela con un alter table ... alter column ... type numeric using ... antes de construir sobre ella.
Migra Firebase Auth: conserva los hashes scrypt, sin reset de contraseña
Esta es la parte que hace feliz a tus usuarios: no tienen que resetear su contraseña. Supabase (GoTrue) sabe verificar los hashes scrypt de Firebase, así que si le pasas los parámetros correctos, tus usuarios entran con la misma contraseña de siempre. Los detalles finos de esto los cubro en la guía hermana, migrar Firebase Auth a Supabase conservando los hashes, pero aquí va el mapa.
Exporta los usuarios a JSON con firestoreusers2json.js y luego impórtalos a la tabla auth.users con import_users.js:
node firestoreusers2json.js [<filename.json>] [<batch_size>]
node import_users.js <path_to_json_file> [<batch_size>]
import_users.js preserva los hashes scrypt de Firebase usando cuatro parámetros que copias de Firebase Console (Authentication, password hash parameters): base64_signer_key, base64_salt_separator, rounds (por ejemplo 8) y mem_cost (por ejemplo 14). Con esos cuatro valores, GoTrue verifica el hash viejo y el login funciona sin reset. Si vas a decidir tu stack de auth desde cero, compara opciones en Better Auth vs Clerk vs Supabase para un SaaS mexicano.
Las otras dos piezas: Storage y Security Rules a RLS
Faltan dos. Storage es directo: el directorio storage/ del repo mueve tus archivos de Firebase Storage a Supabase Storage, que por debajo es S3 más una capa de políticas. Es una copia de objetos más el mapeo de buckets; no tiene la trampa del modelo de datos.
Las Security Rules son otra historia, y quiero ser explícito: no hay import. Las Security Rules de Firebase no se migran, se reescriben como políticas de Row Level Security (RLS) en Postgres. Trátalo como su propia tarea y prueba cada política. Una regla de Firebase que decía “solo el dueño lee su documento” se vuelve una policy basada en auth.uid():
alter table orders enable row level security;
create policy "los usuarios ven solo sus orders"
on orders for select
using ( auth.uid() = user_id );
Cada regla que tenías en Firebase necesita su equivalente en SQL, y RLS aplica por fila a nivel de base de datos —por eso valió la pena desnormalizar orders en su propia tabla: ahora puedes protegerla directamente en vez de intentar filtrar dentro de un jsonb. Escribe las políticas, y después pruébalas con un usuario real, no solo leyendo el SQL.
Checklist de cutover
Antes de apagar Firebase, corre esta lista. Migra en este orden y valida cada paso contra datos reales.
Los puntos que no puedes saltarte:
- Todas las colecciones exportadas y los objetos anidados desnormalizados en tablas con llaves foráneas, no arrastrados como
jsonb. - Llaves primarias, foráneas e índices creados en Postgres; conteos de filas cuadrados contra Firestore.
- Usuarios importados a
auth.userscon los cuatro parámetros dehash; probaste un login real con una contraseña vieja y funciona sinreset. - Archivos de Storage copiados y accesibles.
- Cada Security Rule reescrita como policy RLS y probada con un usuario que sí y uno que no debería tener acceso.
Si estás dimensionando el esfuerzo de un proyecto así, ayuda tener el contexto de cuánto cuesta construir un MVP de SaaS y, para el detalle profundo de la parte de datos, la guía hermana migrar de Firebase a Supabase, de Firestore a Postgres. Una migración bien hecha no es mover datos: es rediseñar el modelo para que Postgres trabaje a tu favor.
Fuentes oficiales: Migrating Firestore data to Supabase, Migrating Firebase Auth to Supabase y el repo supabase-community/firebase-to-supabase. El repo cambia; verifica los nombres de los scripts y la ubicación de los parámetros de hash contra la versión viva antes de correr la migración.