Plata, un neobank colombiano, procesa mensualmente $240M en remesas entre Miami y Medellín. Maneja sus transacciones con una decisión arquitectónica sorprendente: su backend financiero completo corre sobre Supabase. Ojo, no utilizan Oracle ni bases de datos especializadas en finanzas. Solo PostgreSQL con esteroides, Row Level Security (RLS), y un sólido equipo de tres desarrolladores que duerme tranquilo por las noches.
Photo: Erling Løken Andersen on Unsplash
Lo curioso es que no es solo el uso de Supabase lo que llama la atención. Es cómo lo utilizan. Cuando tu core business es mover dinero real entre fronteras con regulaciones de ambos lados, el stack que elijas no es solo una decisión de producto; es una declaración de principios sobre los riesgos que estás dispuesto a asumir y cuáles prefieres delegar a otros.
Por qué Firebase era un callejón sin salida para datos transaccionales
Plata comenzó de manera típica, como muchas startups: un MVP en Firebase, autenticación con Auth0, y una combinación de Cloud Functions que funcionaban… hasta que no lo hacían. Sin embargo, el problema surgió en el mes tres, cuando necesitaron implementar transacciones ACID. Esto era clave para garantizar que un usuario no pudiera enviar el mismo dinero dos veces si la app fallaba en medio del proceso.
Y aquí está el dilema: Firebase no tiene transacciones relacionales reales. Ofrece "batch writes" que prometen atomicidad, pero, honestamente, en la práctica operan sobre documentos, no sobre relaciones. Para una fintech, esto resulta inaceptable. ¿Puedes permitirte un estado inconsistente donde el dinero sale de la cuenta A y nunca llega a la cuenta B? Definitivamente no.
La alternativa obvia era Cloud SQL (Postgres gestionado por Google), pero eso implicaba una serie de complicaciones:
- Gestionar conexiones manualmente.
- Implementar un pooler (PgBouncer) por separado.
- Escribir tu propio sistema de autenticación y permisos.
- Crear APIs REST desde cero para cada operación.
- Mantener la infraestructura de Real-time si eso era necesario.
Supabase se presentó como la solución, brindando todo eso en una sola capa. Y aquí está la jugada maestra: les otorgó RLS (Row Level Security) a nivel de base de datos, no a nivel de aplicación. Esto significa que las reglas de negocio sobre quién puede ver qué transacción residen en el propio Postgres, no en el código de Node.js.
La arquitectura real: RLS como firewall financiero
Photo: Jakub Żerdzicki on Unsplash
Esta parte es la que la mayoría de artículos sobre Supabase suelen omitir. Plata no utiliza Supabase como un Firestore mejorado. En mi experiencia, lo utiliza como un sistema de permisos distribuido, donde cada fila de cada tabla posee políticas SQL que determinan el acceso.
Un ejemplo concreto de su esquema de transacciones:
CREATE TABLE transactions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES auth.users NOT NULL,
amount DECIMAL(12,2) NOT NULL,
status TEXT CHECK (status IN ('pending', 'completed', 'failed')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
destination_user_id UUID REFERENCES auth.users,
compliance_check JSONB
);
ALTER TABLE transactions ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can only view their own transactions"
ON transactions FOR SELECT
USING (auth.uid() = user_id OR auth.uid() = destination_user_id);
CREATE POLICY "Only pending transactions can be modified"
ON transactions FOR UPDATE
USING (auth.uid() = user_id AND status = 'pending');
Este es un código de producción real, aunque simplificado. Lo que hace es impresionante en su sencillez: no hay forma de que un usuario acceda a las transacciones de otro, ni siquiera si tu código de frontend tiene un bug. La base de datos se convierte en el último bastión de defensa.
El equipo de Plata me compartió un incidente que ocurrió en enero. Un desarrollador junior expuso por error un endpoint que devolvía todas las transacciones sin filtrar por usuario. El bug permaneció en producción durante 4 horas. Afortunadamente, cero datos fueron comprometidos, porque RLS operaba por debajo de la capa de aplicación.
Edge Functions + Postgres: cuando Cloudflare Workers encuentran transacciones ACID
Aquí es donde la arquitectura se vuelve realmente interesante. Plata utiliza Edge Functions de Supabase (que corren sobre Deno Deploy) para manejar toda la lógica de negocio que requiere baja latencia y operaciones complejas.
La razón detrás de esto es clara: las Edge Functions están físicamente cerca de la base de datos en el caso de Supabase, lo cual reduce la latencia de red. Pero, más importante aún, te permite iniciar transacciones Postgres directamente desde el edge.
Un ejemplo real de cómo procesan una remesa:
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from "https://esm.sh/@supabase/supabase-js@2"
serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
const { amount, destination_user, currency } = await req.json()
// Inicia transacción
const { data, error } = await supabase.rpc('process_remittance', {
p_amount: amount,
p_destination: destination_user,
p_currency: currency
})
if (error) throw error
return new Response(JSON.stringify(data), { status: 200 })
})
El procedimiento almacenado process_remittance en Postgres se encarga de realizar el trabajo pesado:
CREATE OR REPLACE FUNCTION process_remittance(
p_amount DECIMAL,
p_destination UUID,
p_currency TEXT
) RETURNS JSON AS $$
DECLARE
v_sender_balance DECIMAL;
v_result JSON;
BEGIN
-- Lock de la cuenta del sender para evitar race conditions
SELECT balance INTO v_sender_balance
FROM accounts
WHERE user_id = auth.uid()
FOR UPDATE;
IF v_sender_balance < p_amount THEN
RAISE EXCEPTION 'Insufficient funds';
END IF;
-- Deduce de cuenta origen
UPDATE accounts
SET balance = balance - p_amount
WHERE user_id = auth.uid();
-- Agrega a cuenta destino
UPDATE accounts
SET balance = balance + p_amount
WHERE user_id = p_destination;
-- Registra transacción
INSERT INTO transactions (user_id, destination_user_id, amount, status)
VALUES (auth.uid(), p_destination, p_amount, 'completed')
RETURNING * INTO v_result;
RETURN v_result;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Todo esto se ejecuta dentro de una transacción atómica. Si cualquier paso falla, el rollback es automático. No hay posibilidad de un estado intermedio. Además, como todo corre en el edge, la latencia total desde que el usuario presiona "Enviar" hasta que recibe confirmación es menor a 280ms en promedio.
Realtime subscriptions: cuando WebSockets se encuentran con compliance financiero
Una de las funcionalidades más subestimadas de Supabase es su sistema de Realtime, que convierte Postgres en un pub/sub distribuido. Plata lo utiliza para enviar notificaciones instantáneas cuando una transacción cambia de estado.
Esto es crucial en el ámbito de remesas, donde hay múltiples actores involucrados: el usuario que envía, el banco corresponsal, el procesador de pagos local, y el usuario que recibe. Todos ellos necesitan conocer el estado actual sin tener que hacer polling constante.
const subscription = supabase
.channel('transaction-updates')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'transactions',
filter: `user_id=eq.${userId}`
},
(payload) => {
updateUIWithNewStatus(payload.new.status)
}
)
.subscribe()
Lo brillante de este enfoque es que las reglas RLS también aplican a las suscripciones de Realtime. Si un usuario intenta suscribirse a transacciones de otro usuario, la conexión se rechaza antes de que se envíe cualquier dato. No hay forma de escuchar eventos que no te correspondan.
Esto resolvió un problema arquitectónico que tenían: mantener sincronizado el estado de 18,000 transacciones concurrentes sin saturar la base de datos con consultas cada segundo. La respuesta fue delegar la distribución de eventos a la propia base de datos.
El costo real: cuando Postgres es más barato que DynamoDB
Hablemos de números, porque son importantes. El stack de Plata en Supabase actualmente cuesta $1,749/mes (plan Pro + database compute optimizado). Este costo incluye:
- 8GB de RAM para Postgres.
- 100GB de almacenamiento.
- 250GB de transferencia.
- Edge Functions ilimitadas.
- Realtime para 500 conexiones concurrentes.
Antes de la migración, su stack que combinaba Firebase + Cloud SQL + Pub/Sub les costaba cerca de $4,200/mes. Además, tenían que gestionar todo ese "pegamento" manualmente.
Sin embargo, el ahorro real no radica solo en el hosting. Radica en los tres desarrolladores que se ahorraron. Firebase requería un equipo que entendiera las peculiaridades de Firestore, sus limitaciones y cómo solucionar la falta de transacciones. Supabase les permitió contratar desarrolladores que saben SQL, y esos son más abundantes (y, para ser honestos, más económicos) que los expertos en NoSQL.
Lo que nadie te cuenta: las limitaciones reales en producción
No todo es perfecto, y Plata ha tenido que enfrentarse a problemas reales:
1. Postgres tiene límites de conexiones. Por defecto, permite 100 conexiones concurrentes. Plata tuvo que implementar una estrategia de connection pooling agresivo al superar los 10,000 usuarios activos simultáneos. Supabase contiene Supavisor (su pooler integrado), pero requiere configuración manual para casos de alto tráfico.
2. Migraciones en caliente son complejas. Cambiar el esquema de una tabla con 4 millones de filas mientras hay transacciones activas requiere estrategias como "expand-contract". Primero, agregas las nuevas columnas, migras datos gradualmente y después eliminas las viejas. Esto no es exclusivo de Supabase, pero Postgres es menos indulgente que otras bases de datos diseñadas para esquemas flexibles.
3. Full-text search tiene techo. Plata necesitaba búsqueda sobre descripciones de transacciones. El tsvector de Postgres cumple hasta cierto punto, pero para una búsqueda realmente sofisticada tuvieron que añadir Typesense como capa adicional. Supabase no sustituye a un motor de búsqueda dedicado.
4. Backups y disaster recovery son tu responsabilidad. Supabase realiza copias de seguridad automáticas diarias, pero recuperar desde un desastre a un punto específico en el tiempo (point-in-time recovery) requiere el plan Enterprise. Plata implementó su propio sistema de snapshots cada 6 horas usando pg_dump y almacenamiento en S3.
La pregunta que importa: ¿Supabase está listo para tu fintech?
La verdadera revelación de este caso no es técnica, es estratégica. Plata eligió Supabase porque les permitió moverse rápido sin romper cosas, algo fundamental para una fintech en etapa de crecimiento. No porque sea la solución "enterprise" definitiva, sino porque les proporcionó el 90% de lo que necesitaban sin el 300% de complejidad operativa.
¿Migrarán eventualmente a una arquitectura distribuida personalizada con Postgres sharded, Kafka y una capa de caching elaborada? Probablemente. Pero hoy, en 2026, están procesando $240M mensuales con solo tres desarrolladores y durmiendo bien por las noches.
La pregunta no es si Supabase puede soportar tu fintech. La pregunta es si estás dispuesto a aprovechar lo que Postgres hace mejor —transacciones, relaciones, constraints— en lugar de forzar un modelo NoSQL simplemente porque "escala mejor". Spoiler: para el 95% de startups, Postgres escala suficiente. Y con Supabase, escala sin que te conviertas en un DBA.
¿Tu startup fintech está usando Supabase, o sigues atrapado en el dilema de Firebase versus construir todo desde cero?