Supabase vs Firebase 2026: Deja de Llamarlo "Firebase para PostgreSQL" — Es una Trampa para Frontend Developers

Supabase vs Firebase 2026: Deja de Llamarlo "Firebase para PostgreSQL" — Es una Trampa para Frontend Developers

Programming· 14 min read

Supabase No Es un Drop-in de Firebase. Es una Arquitectura Diferente que el 90% No Espera

Llevo tres aplicaciones en producción con Supabase.

ConversoriaECNAE.es — un SaaS multiinquilino con datos fiscales complejos.

GestoriasCercaDeMi.com — un directorio con búsqueda geoespacial y relaciones entre tablas.

FindEmergencyPlumber.com — un buscador con perfiles, reseñas y disponibilidad en tiempo real.

Y en las tres, el patrón fue idéntico.

*Empecé usando Supabase como "Firebase con SQL". Y terminé escribiendo más PostgreSQL que Express en toda mi carrera. *

El marketing de Supabase dice: "Open source Firebase alternative. Build faster. Ship more."

La realidad es más sutil. Y más exigente.

---

El Gran Error: Pensar que Supabase Simplifica el Backend

La mayoría asume que Supabase reemplaza tu backend entero.

Instalas el SDK. Llamas a supabase.from('posts').select('*'). Y ya tienes una API.

*Funciona. Hasta que necesitas algo real. *

Multiinquilino. Filtrado por rol. Una join entre tres tablas. Validación de negocio.

Ahí el cliente SDK se queda corto. Necesitas Row-Level Security. Y RLS es SQL puro.

Lo que la mayoría cree:

Usas el SDK cliente, Supabase decide qué datos puede ver el usuario, todo es mágico.

Lo que realmente pasa:

Cada select('*') ejecuta una query PostgreSQL que pasa por tus RLS policies. Si las policies están mal escritas — sin índices, con subqueries que barren tablas enteras — tu app se ralentiza hasta romperse.

>Aprendí esta lección con una tabla de 12.000 filas y una RLS policy que usaba EXISTS (SELECT 1 FROM ...) sin indexar la columna referenciada. La query pasó de 8ms a 1.2 segundos por fila. Literalmente O(n²).

Por qué el modelo mental de "Firebase con SQL" es peligroso

El error no es técnico, es conceptual. Firebase te permite ignorar la base de datos. Es un árbol JSON que sincronizas con un SDK. No hay esquema, no hay migraciones, no hay índices que optimizar. El desarrollador frontend puede construir una app completa sin escribir una línea de SQL.

Supabase, en cambio, te da la ilusión de esa misma simplicidad. El primer día instalas el SDK, haces un par de selects, y todo parece igual de mágico. El problema llega cuando añades la tercera tabla, o cuando necesitas que un usuario solo vea los posts de su organización. De repente, la magia se desvanece y te encuentras escribiendo políticas RLS que son consultas SQL complejas que se ejecutan en producción.

Esta falsa familiaridad es la trampa más peligrosa. El desarrollador que viene de Firebase asume que puede aprender RLS sobre la marcha, igual que aprendió las security rules en una tarde. Pero RLS no es un JSON con tres reglas. RLS es SQL que corre dentro de PostgreSQL, y PostgreSQL no perdona. Una policy mal escrita no solo falla silenciosamente — degrada el rendimiento de toda la base de datos.

El mito del "backendless" y por qué no existe

Supabase se promociona como una alternativa backendless, pero esa etiqueta es engañosa. Lo que realmente hace es mover el backend al motor de la base de datos. En lugar de tener un servidor Express con rutas, middlewares y controladores, tienes triggers, funciones SQL y RLS policies. El código no desaparece — simplemente cambia de forma.

Para apps triviales (un blog personal, un portfolio, una landing page con formulario), esto funciona de maravilla. Pero para aplicaciones con lógica de negocio real — validaciones entre entidades, reglas de facturación, permisos granulares — te encuentras escribiendo más lógica en PostgreSQL de la que escribirías en un backend tradicional. Y el problema es que PostgreSQL no es un lenguaje de propósito general. Depurar una función SQL que hace validaciones complejas es más difícil que depurar el mismo código en TypeScript.

---

RLS No Son Security Rules de Firebase. Son SQL en Producción

Firebase tiene security rules. Un JSON con sintaxis custom. Lo aprendes en una tarde.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Supabase RLS es esto:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

No es más complejo por capricho. *Es más complejo porque se ejecuta dentro de PostgreSQL, por fila, en el motor de base de datos. *

Y ahí está el problema que nadie te cuenta:

  1. RLS se evalúa por fila — no por ruta como Firebase. Una tabla con 100k filas y una RLS policy con subqueries puede matarte.
  2. Necesitas entender `USING` vs `WITH CHECK` — uno controla SELECT, el otro INSERT/UPDATE. Confundirlos expone datos o bloquea escrituras legítimas.
  3. Las policies pueden cachearse mal — PostgreSQL cachea planes de ejecución. Una policy que funciona en local puede petar en prod con datos reales.

La diferencia fundamental: evaluación en servidor vs evaluación en base de datos

Firebase evalúa las security rules en el servidor de autenticación antes de conceder acceso a un nodo del árbol JSON. Es una decisión binaria: puedes leer este nodo o no. La base de datos subyacente (Firestore o Realtime Database) no participa en la decisión.

Supabase integra RLS directamente en el planificador de consultas de PostgreSQL. Cuando ejecutas SELECT * FROM posts WHERE org_id = 'x', PostgreSQL evalúa la RLS policy para cada fila que cumple el WHERE. Si la policy tiene una subquery que hace un escaneo secuencial de 50.000 filas, cada una de las filas que cumple el WHERE paga ese coste.

Esto significa que una RLS policy que es correcta lógicamente puede ser catastrófica en rendimiento. Y no hay forma de saberlo sin ejecutar EXPLAIN ANALYZE y leer el plan de ejecución.

Error común de frontend developer:

"Pongo RLS en todas las tablas y el SDK cliente se encarga"

Lo que funciona:

Escribes cada RLS policy, la pruebas con EXPLAIN ANALYZE, creas índices compuestos para que las subqueries no barran tablas, y usas SECURITY DEFINER functions para operaciones complejas que necesitan bypassar RLS sin exponer datos.

El caso real que me rompió el esquema mental

En GestoriasCercaDeMi.com, tengo una tabla reviews con 35.000 filas. Cada reseña pertenece a una gestoría, y cada gestoría pertenece a una categoría. Necesitaba que un usuario viera solo reseñas de gestorías en su provincia.

La RLS policy inicial era:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Lógicamente correcta. Pero EXPLAIN ANALYZE mostró un Seq Scan sobre gestorias y otro sobre perfiles. 35.000 filas × 2 escaneos secuenciales = 700ms por query. Para una app de directorio, inaceptable.

La solución fue añadir provincia directamente a la tabla reviews como columna denormalizada y cambiar la policy a:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Con un índice en reviews(provincia), la query bajó a 3ms. Pero esto implica denormalización consciente, algo que un desarrollador de Firebase conoce bien pero que un purista de SQL odiará. Supabase te fuerza a encontrar el equilibrio entre normalización y rendimiento de RLS.

---

Las Migraciones: Lo que Firebase Jamás Te Pedirá

Con Firebase, tu esquema es implícito. Creas colecciones desde el cliente. Los campos aparecen cuando los escribes.

*Con Supabase, el esquema es ley. *

Y la forma correcta de gestionarlo no es desde el Dashboard. Es desde ficheros de migración.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop
[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop
[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Esto no es un extra opcional. *Si no haces migraciones, estás operando sin control de versiones en tu base de datos. *

El Dashboard de Supabase es tentador para "probar cosas rápido". Lo usé las dos primeras semanas. Luego alguien cambió una columna desde la UI, la migración local falló, y perdí medio día debugging.

Aprendizaje: las migraciones son el contrato de tu esquema. El Dashboard es solo para debugging de lectura.

El flujo de trabajo que deberías adoptar desde el día uno

Si vienes de Firebase, el concepto de "migración" te sonará a over-engineering. Pero en Supabase, es la única forma viable de trabajar en equipo.

El flujo recomendado es:

  1. Trabajas localmente con supabase start. Esto levanta PostgreSQL, GoTrue (auth), Realtime y Storage en Docker.
  2. Creas migraciones cada vez que tocas el esquema. Una migración por cambio atómico.
  3. Commiteas las migraciones al repositorio. Todo el equipo tiene el mismo esquema.
  4. Aplicas migraciones a producción con supabase db push. Si hay conflictos, supabase db diff te muestra qué cambió.

Si alguien del equipo cambia algo desde el Dashboard de Supabase (por ejemplo, añade una columna), el fichero de migración local se descuadra. La próxima vez que hagas supabase db push, Supabase detecta que el esquema remoto no coincide con el acumulado de migraciones y te pide resolverlo.

Esto es bueno: te obliga a mantener la disciplina. Pero es malo si tu equipo está acostumbrado a "tocar la base de datos desde la UI" como se hace con Firebase o Airtable.

---

Realtime en Supabase: WAL no es WebSocket

Cuando escuchas cambios en tiempo real con Supabase, no estás conectado a un pub/sub genérico. *Estás escuchando el Write-Ahead Log de PostgreSQL. *

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Esto es más preciso que Firebase Realtime Database. Recibes cambios reales de la base de datos, no eventos de un árbol JSON sincronizado en memoria.

Pero tiene tradeoffs que Firebase no tiene:

  • Cada subscripción abre un replication slot en PostgreSQL. Si un cliente se desconecta sin limpiar la subscripción, el slot acumula WAL y llena el disco.
  • Supabase Pro limita Realtime a 200 conexiones concurrentes (no channels — conexiones). Una app React con 200 pestañas abiertas alcanza el límite.
  • Las subscripciones con filtros (filter: 'org_id=eq.${orgId}') se evalúan en la base de datos, no en el cliente. Un filtro mal escrito vuelve a generar escaneos de tabla.

Cómo se comporta en apps reales: el caso de las pestañas múltiples

Un escenario que me rompió los esquemas en FindEmergencyPlumber.com fue el de los usuarios con múltiples pestañas abiertas.

Cada pestaña de React abría una nueva conexión Realtime. Un usuario con 5 pestañas ocupaba 5 de las 200 conexiones del plan Pro. Si cinco usuarios hacían lo mismo, consumían 25 conexiones. No por channels, sino por conexiones TCP reales a PostgreSQL.

Firebase maneja esto de forma transparente: usa una sola conexión WebSocket por cliente, multiplexada. Supabase, al estar atado al WAL de PostgreSQL, no puede multiplexar conexiones de la misma forma.

La solución para producción es usar un solo canal Realtime compartido entre pestañas (via BroadcastChannel API o SharedWorker) o directamente no usar Realtime del lado del cliente y optar por polling con SWR/React Query para la mayoría de los casos.

Lo que Firebase hace bien:

Conexiones ilimitadas (precio distinto). Sin riesgo de llenar disco con WAL. Filtros evaluados en cliente.

Lo que Supabase hace mejor:

Cambios reales de base de datos, no un árbol sincronizado. Consistencia transaccional. Puedes hacer Realtime sobre consultas SQL complejas.

---

Edge Functions en Deno: El Coste Oculto del Ecosistema

Las Edge Functions de Supabase corren en Deno, no en Node.js.

Esto suena bien en teoría. Cold starts de ~5ms vs 300ms+ en Node.js. APIs más rápidas. Sin runtime pesado.

*En la práctica, te encuentras con esto: *

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Los problemas que no ves hasta que despliegas:

  • Prisma no corre en Deno. Tu ORM favorito no funciona. Usas postgres de Deno o supabase-js directamente.
  • Packages de Node.js que usan `process.env` o `__dirname` — no funcionan. Tienes que usar Deno.env.get() y import.meta.url.
  • Observability SDKs — Datadog, Sentry, New Relic. Muchos no tienen soporte nativo para Deno. Te toca implementar telemetría manual.
  • npm packages via `npm:` specifier — funciona, pero no todo. Lo que depende de native addons (bcrypt, sharp) no va.

El dilema del stack: Deno vs Node.js

Para equipos que vienen de Node.js/Express, Edge Functions son un paso atrás en compatibilidad. *Ganas cold starts, pierdes ecosistema. *

Si tu equipo ya usa Deno o tiene experiencia con entornos serverless (CloudFlare Workers, Deno Deploy), la transición es natural. Pero si tu stack actual usa bcrypt para hash de contraseñas, Prisma como ORM y Sentry para errores — todo eso deja de funcionar.

La alternativa es escribir Edge Functions que hagan poco y deleguen la lógica compleja a:

  • Consultas SQL directas con el cliente postgres de Deno
  • Llamadas a servicios externos (pero sin SDKs de terceros)
  • Validaciones simples que no requieran librerías pesadas

Para lógica compleja, muchos equipos terminan manteniendo un backend Node.js aparte y solo usan Edge Functions para tareas que necesitan baja latencia (webhooks, transforms rápidos). Esto añade complejidad operativa, pero es más realista que intentar forzar todo el backend en Deno.

---

El Framework: Cómo Usar Supabase Bien

Después de tres apps en producción, este es el flujo que funciona:

Paso 1: Diseña el esquema como migraciones, no desde la UI

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Escribe CREATE TABLE, CREATE INDEX, CREATE FOREIGN KEY en SQL. Versiona todo.

Paso 2: Implementa RLS antes de escribir una línea de frontend

Cada tabla necesita enable row level security + al menos una policy. Si no pones RLS, las queries del cliente devuelven cero filas.

Paso 3: Genera tipos TypeScript desde tu base de datos

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Esto te da seguridad de tipos del backend al frontend. El schema cambia → regeneras tipos → TypeScript te avisa donde se rompe.

Paso 4: Usa supabase start para desarrollo local

Docker-based. PostgreSQL local, auth local, Edge Functions locales. No tocas producción hasta que todo funciona.

Paso 5: Prueba RLS con EXPLAIN ANALYZE

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Si ves Seq Scan en una tabla con 10k+ filas, te falta un índice.

El antipatrón más común: RLS como único mecanismo de autorización

Un error que he visto repetido es usar RLS como la única capa de autorización, incluso para operaciones que requiren lógica transaccional.

Por ejemplo: cuando un usuario crea un post, necesitas validar que su organización no haya alcanzado el límite de posts del plan. En Firebase harías esto en una Cloud Function. En Supabase, la tentación es meterlo en una RLS policy con WITH CHECK.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Esto funciona, pero es frágil. Si el plan cambia, la policy se rompe. Si hay concurrencia, dos inserts simultáneos pueden saltarse el límite (race condition).

Lo correcto es usar una función `SECURITY DEFINER` para operaciones que requieren lógica transaccional y llamarla desde una Edge Function o un backend tradicional. RLS es para filtrar acceso, no para implementar lógica de negocio compleja.

---

¿Cuándo Usar Supabase y Cuándo No?

Usa Supabase cuando:

  • Tu app necesita relaciones complejas entre entidades
  • Quieres Realtime basado en cambios reales de base de datos
  • Te sientes cómodo escribiendo SQL y entiendes PostgreSQL
  • Necesitas pgvector para embeddings y búsqueda semántica
  • Tu equipo puede mantener migraciones y probar RLS con EXPLAIN ANALYZE

No uses Supabase cuando:

  • Tu modelo de datos es simple (user + posts + likes — eso con Firebase va bien)
  • Tu equipo no sabe SQL y no quiere aprender
  • Tu app necesita operaciones transaccionales complejas (ahí mejor un backend tradicional)
  • Necesitas ecosistema Node.js completo (Prisma, bcrypt, librerías específicas)
  • Tu app tiene picos de conexiones Realtime difíciles de predecir

La decisión no es técnica, es de equipo

Al final, la elección entre Supabase y Firebase no es sobre bases de datos. Es sobre qué está dispuesto a aprender tu equipo.

Firebase te permite ignorar la base de datos. La experiencia es "escribo código cliente, los datos se sincronizan". El coste es que cuando necesitas algo que Firebase no soporta de serie (joins complejos, búsqueda geoespacial, transacciones entre colecciones), estás atascado.

Supabase te da control total sobre PostgreSQL. La experiencia es "diseño el esquema, escribo RLS, genero tipos, y luego consumo desde el cliente". El coste es que tienes que entender PostgreSQL a nivel de producción: índices, planes de ejecución, concurrencia, WAL.

Si tu equipo está formado mayoritariamente por desarrolladores frontend que nunca han escrito una migración SQL, Firebase será más productivo a corto plazo. Si tienes al menos una persona que entiende PostgreSQL y puede mantener la capa de base de datos, Supabase escala mucho mejor.

---

La Tesis Final

Supabase no es Firebase para SQL.

*Supabase es PostgreSQL con un SDK cliente, auth, y Realtime pegados encima. *

Eso es increíblemente potente si entiendes PostgreSQL. Y es una trampa si esperas que la base de datos desaparezca debajo de una capa de abstracción.

Firebase te esconde la base de datos. Supabase te obliga a dominarla.

Elige según lo que quieras aprender.

Artículos relacionados

---

¿Quieres recibir contenido como este cada semana? Suscríbete a mi newsletter

Brian Mena

Brian Mena

Software engineer building profitable digital products: SaaS, directories and AI agents. All from scratch, all in production.

LinkedIn