Personal finance product. 2025 to 2026. Author. Expo, React Native, Supabase, Wompi.

Capitalia

A personal finance app with real subscription billing: budgets, accounts and goals on Supabase, and a payment layer that moved from Stripe to Wompi without the schema having to care which one it is.

Role
Author
Built in
2025 to 26
Providers
Wompi, Stripe
Data
Postgres RLS
Client
Expo
Source on GitHub Index
Fig. 01
2400 × 960App dashboard: budgets, accounts, goal progress
Fig. 01. Budgets, accounts and goals, with subscription state resolved server side.

The problem

A subscription app has to charge people, and in Colombia that means Wompi rather than Stripe. The app was built on Stripe first, so the interesting work was not adding a second gateway. It was removing the assumption that there was only ever one.

I migrated the payment layer to Wompi in October 2025 and used the migration to generalise the schema instead of renaming it. Everything that touches a secret runs in a Supabase Edge Function; the device only ever invokes one.

ExpoReact NativeTypeScriptSupabasePostgres RLSEdge FunctionsWompiStripe

Two gateways, one schema

The migration renamed stripe_customers to payment_customers and added a payment_provider column, so a second gateway is a row value rather than a second set of tables.

No payment secret on the device

Checkout, cancellation, status and webhooks are all Edge Functions. The app holds public keys only, and a stolen build gives an attacker nothing to charge with.

Authorisation lives in the database

Row Level Security on every sensitive table means a query cannot return another person's transactions even if a screen forgets to filter.

Features stay where they belong

A strict scope rule decides placement: used by two features it moves to shared, used by one it stays local. The structure does not drift as the app grows.

Architecture

Core service

Edge Functions

Checkout, cancellation, status, webhooks

Deno functions holding the gateway secret. create-wompi-checkout, cancel-subscription-wompi, get-subscription-status-wompi and wompi-webhooks, with the Stripe equivalents still present alongside them.

DenoEdge Functions

Decisions and the price I paid for them

Generalise the schema during the migration, not after

Moving to Wompi could have been a rename. Instead the customer table became payment_customers with an explicit payment_provider column, so the data model stopped encoding a vendor name.

Trade-off accepted

A rename migration against live tables is riskier than adding new ones, and two provider implementations now have to keep working rather than one.

Every secret-bearing call is an Edge Function

Creating a checkout on the client would put a secret key in a binary anyone can unpack, so the app can only invoke a function and read the result.

Trade-off accepted

Every payment path is a deployed, versioned function with its own logs and failure mode, instead of one SDK call.

Row Level Security instead of guards in screens

Authorisation in Postgres holds for every caller, including a future web client or a script, rather than depending on each screen remembering to filter by user.

Trade-off accepted

A policy bug is a database bug: harder to unit-test and slower to diagnose than a forgotten guard clause in application code.

One placement rule, no exceptions

Code used by two or more features moves to shared, code used by one stays inside that feature. Making it absolute is what keeps shared from becoming the folder everything drifts into.

Trade-off accepted

Promoting code is a deliberate act, so a small change occasionally costs a refactor it would not otherwise need.

Inside the build

  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.