Producto de finanzas personales. 2025 a 2026. Autor. Expo, React Native, Supabase, Wompi.
Capitalia
Una app de finanzas personales con cobro de suscripción real: presupuestos, cuentas y objetivos sobre Supabase, y una capa de pagos que pasó de Stripe a Wompi sin que el esquema tuviera que enterarse de cuál es.
El problema
Una app de suscripción tiene que cobrar, y en Colombia eso significa Wompi antes que Stripe. La app se construyó primero sobre Stripe, así que el trabajo interesante no fue añadir una segunda pasarela. Fue quitar la suposición de que solo iba a haber una.
Migré la capa de pagos a Wompi en octubre de 2025 y aproveché la migración para generalizar el esquema en lugar de renombrarlo. Todo lo que toca un secreto corre en una Edge Function de Supabase; el dispositivo solo invoca.
Dos pasarelas, un esquema
La migración renombró stripe_customers a payment_customers y añadió una columna payment_provider, así que una segunda pasarela es un valor de fila y no un segundo juego de tablas.
Ningún secreto de pago en el dispositivo
Checkout, cancelación, estado y webhooks son todos Edge Functions. La app solo tiene claves públicas, y un build robado no le sirve a nadie para cobrar.
La autorización vive en la base de datos
Row Level Security en cada tabla sensible significa que una consulta no puede devolver las transacciones de otra persona aunque una pantalla se olvide de filtrar.
El código se queda donde corresponde
Una regla de alcance estricta decide la ubicación: si lo usan dos features se mueve a shared, si lo usa una se queda local. La estructura no deriva a medida que la app crece.
Arquitectura
Servicio central
Edge Functions
Checkout, cancelación, estado, webhooks
Funciones Deno con el secreto de la pasarela. create-wompi-checkout, cancel-subscription-wompi, get-subscription-status-wompi y wompi-webhooks, con las equivalentes de Stripe todavía presentes al lado.
Decisiones y el precio que pagué por ellas
Generalizar el esquema durante la migración, no después
Pasar a Wompi podía haber sido un renombrado. En vez de eso la tabla de clientes pasó a ser payment_customers con una columna payment_provider explícita, para que el modelo de datos dejara de llevar dentro el nombre de un proveedor.
Trade-off aceptado
Una migración de renombrado sobre tablas vivas es más arriesgada que añadir nuevas, y ahora hay que mantener funcionando dos implementaciones de proveedor en lugar de una.
Toda llamada con secreto es una Edge Function
Crear un checkout en el cliente pondría una clave secreta en un binario que cualquiera puede abrir, así que la app solo puede invocar una función y leer el resultado.
Trade-off aceptado
Cada camino de pago es una función desplegada y versionada, con sus propios logs y su propio modo de fallo, en lugar de una llamada a un SDK.
Row Level Security en vez de guardas en las pantallas
La autorización en Postgres aplica a todo el que llame, incluido un futuro cliente web o un script, en lugar de depender de que cada pantalla se acuerde de filtrar por usuario.
Trade-off aceptado
Un fallo de política es un fallo de base de datos: más difícil de probar en unitarias y más lento de diagnosticar que una guarda olvidada en el código.
Una regla de ubicación, sin excepciones
El código que usan dos o más features se mueve a shared; el que usa una se queda dentro de esa feature. Hacerla absoluta es lo que evita que shared se convierta en el cajón donde acaba todo.
Trade-off aceptado
Promover código es un acto deliberado, así que un cambio pequeño a veces cuesta una refactorización que de otro modo no haría falta.
Por dentro
-
Fig. 02 The migration that generalised the payment schema.
-
Fig. 03 Both gateways deployed side by side as functions.
-
Fig. 04 Feature-local structure, with a strict promotion rule.