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.

Rol
Autor
Construido en
2025 a 26
Proveedores
Wompi, Stripe
Datos
Postgres RLS
Cliente
Expo
Código en GitHub Índice
Fig. 01
2400 × 960App dashboard: budgets, accounts, goal progress
Fig. 01. Presupuestos, cuentas y objetivos, con el estado de suscripción resuelto en servidor.

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.

ExpoReact NativeTypeScriptSupabasePostgres RLSEdge FunctionsWompiStripe

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.

DenoEdge Functions

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

  1. Fig. 02
    -- 20251011000001_migrate_to_wompi.sql
    
    ALTER TABLE stripe_customers
      RENAME TO payment_customers;
    
    ALTER TABLE payment_customers
      ADD COLUMN payment_provider
      VARCHAR(50) DEFAULT 'wompi';
    900 × 760
    Fig. 02

    The migration that generalised the payment schema.

  2. Fig. 03
    supabase/functions/
    
      create-wompi-checkout/
      cancel-subscription-wompi/
      get-subscription-status-wompi/
      wompi-webhooks/
    
      create-checkout-session/
      stripe-webhooks/
    900 × 760
    Fig. 03

    Both gateways deployed side by side as functions.

  3. Fig. 04
    900 × 760src/features: budgets, transactions, goals, subscriptions
    Fig. 04

    Feature-local structure, with a strict promotion rule.