Rastreador de Gastos para Viaje por Asia: Balance en Vivo, Multi-divisa y Cloudflare Zero Trust

Resumen
En lugar de depender de una app de terceros con datos en servidores ajenos, construí una herramienta propia en una semana. La decisión técnica más interesante fue separar el importe original del gasto (en THB, USD o CHF) del equivalente en euros: al abrir la página de balance, la app pide los tipos de cambio actuales a Frankfurter y recalcula toda la deuda histórica, así que el número que ves siempre refleja lo que se debe hoy al precio actual del dinero.
El Problema
Estoy de viaje por Asia con un amigo. Cada día hay gastos en baht tailandeses, dólares o euros: quién paga el hotel, quién invita a comer, qué es gasto compartido y qué es personal. Las apps genéricas como Splitwise no resuelven el problema real: si pagas 1.000 THB hoy y mañana el baht se mueve, la deuda en euros cambia. Una hoja de cálculo tampoco: hay que actualizarla a mano, no tiene historial limpio ni cálculo automático por persona.
La Solución
Construí una aplicación web completa con Next.js y Supabase que registra cada gasto en su divisa original (EUR, THB, USD, CHF) y recalcula el balance entre los dos viajeros usando los tipos de cambio actuales de la API de Frankfurter cada vez que se abre la página. Si el baht sube o baja, la deuda de hoy refleja lo que se debe hoy, no lo que valía el día del pago. El acceso está restringido con Cloudflare Zero Trust a los dos correos autorizados, sin necesidad de gestionar un sistema de login propio.
App en uso
Dashboard, balance en vivo y estadísticas
Las tres capturas muestran las partes más técnicas de la app: el dashboard con KPIs del viaje, el balance calculado al tipo de cambio actual y los gráficos de estadísticas por categoría y persona.
01
Dashboard del viaje
Vista principal con los KPIs del viaje: total gastado, gasto del día, de la semana y media diaria. Incluye el desglose de las categorías principales con barras de progreso y los últimos movimientos con su categoría, país y quién pagó.
02
Balance y deuda en tiempo real
El balance muestra en todo momento quién le debe cuánto a quién, calculado al tipo de cambio actual de Frankfurter API. El badge 'Tasas en vivo' confirma que los datos son frescos. La tarjeta principal resume el saldo neto y el listado detalla cada gasto pendiente de liquidar.
03
Estadísticas con gráficos Recharts
Panel de estadísticas con gráfico de barras por categoría, evolución del gasto a lo largo del viaje y distribución circular. Los gráficos se pueden filtrar por persona: los gastos compartidos se dividen al 50% al aplicar el filtro.
Herramientas
Qué utilicé y por qué
Elegí herramientas que me permitieran tener algo funcional y desplegado en pocos días, sin sacrificar la parte técnica que me importaba: el cálculo de balance en vivo.
Next.js 16 + React 19
App Router con Server Components para el dashboard (datos siempre frescos sin caché) y Client Components para el balance e interactividad. Todo en un solo proyecto sin backend separado.
Supabase
Backend gestionado: PostgreSQL, API REST autogenerada y cliente JavaScript. No tuve que gestionar un servidor de base de datos ni escribir un backend propio. RLS configurado aunque desactivado para uso personal entre dos usuarios de confianza.
Frankfurter API
API pública y gratuita para tipos de cambio en tiempo real sin necesidad de API key. Con cadena de fallback: API en vivo → última tasa guardada en Supabase → valores hardcoded. El resultado siempre muestra si el dato es 'en vivo' o 'histórico'.
Recharts
Gráficos de barras, líneas y circular para las estadísticas. Elegido por su integración natural con React y porque los datos ya están en el estado del componente sin necesidad de configuración extra.
Cloudflare Zero Trust
Capa de acceso antes de llegar a la app. Solo los dos correos autorizados pueden entrar. Elimina la necesidad de construir un sistema de autenticación propio: ni JWT, ni base de usuarios, ni formulario de login.
Dokploy + Docker + VPS
Desplegado en el mismo VPS que otras herramientas internas. Docker para el contenedor, Dokploy para gestionar el despliegue sin comandos manuales y Cloudflare para el DNS y el proxy.
Funcionalidades
Qué incluye el producto
Cada módulo resuelve una necesidad concreta del viaje. No es un clon genérico: las decisiones de diseño responden a problemas reales que aparecieron durante los primeros días.
Balance en tiempo real con tipos de cambio vivos
La característica más importante. Al abrir el balance, la app consulta Frankfurter API para obtener EUR/THB, EUR/USD y EUR/CHF actuales, y recalcula toda la deuda histórica con esas tasas. Si pagué 1.000 THB cuando el baht valía 38€ y hoy vale 40€, la deuda refleja 25€, no 26,31€. Con fallback a Supabase si la API falla, y a valores hardcoded como último recurso.
Tres tipos de gasto por cada registro
Cada gasto es 'compartido' (50/50), 'personal de A' o 'personal de B'. El balance distingue quién pagó físicamente de quién es responsable: si A paga algo que es de B, la deuda sube aunque ambos usen la misma tarjeta. Los gastos liquidados se pueden marcar como 'is_settled' sin borrarlos.
Multi-divisa nativa con cuatro columnas de importe
Cada gasto almacena el importe original (amount_thb, amount_usd, amount_chf, amount_eur) más la divisa de entrada. Así el recálculo al tipo de cambio vivo siempre parte del importe original, no de una conversión ya guardada que nadie actualizaría.
Dashboard con KPIs y desglose por categoría
Total del viaje, gasto del día, de la semana y media diaria calculados al vuelo. Desglose de las cinco categorías principales con barras de progreso, últimos ocho movimientos, panel de tipos de cambio y estado del alojamiento activo si hay uno en curso.
Estadísticas con gráficos Recharts
Gráfico de barras por categoría, evolución del gasto por semana (línea) y gráfico circular de distribución. Los tres se pueden filtrar por persona: los gastos compartidos se dividen al 50% al aplicar el filtro.
Alojamientos, países, categorías y diario
Registro completo de hoteles con check-in, check-out y precio por noche vinculado al gasto. Panel de países visitados con su moneda local. Categorías personalizables con emoji e color. Conversor de divisas integrado. Diario de viaje.
Dudas y decisiones
Problemas que tuve que resolver
El problema técnico principal no era guardar gastos, sino calcular correctamente quién debe cuánto cuando las divisas se mueven.
Deuda que cambia con el mercado
Si guardara la conversión a euros en el momento del gasto, la deuda sería estática y técnicamente incorrecta: el dinero que se debe hoy no tiene el valor de hace tres semanas. La solución fue guardar siempre el importe original en su divisa y recalcular al tipo de cambio actual en cada carga de la página.
Separar quién pagó de quién es responsable
El modelo más simple sería: 'A pagó X, B pagó Y, se divide por 2'. Pero eso no cubre gastos personales de uno pagados por el otro, ni liquidaciones parciales. El modelo real tiene cuatro variables por persona: lo que pagó, lo que le corresponde, lo que ya ha liquidado y el saldo neto resultante.
Fallback en cascada para los tipos de cambio
Frankfurter puede fallar o tardar. Si falla, la app cae al último tipo guardado en Supabase. Si tampoco hay datos en Supabase (app recién instalada), usa valores hardcoded y lo indica visualmente con un badge 'Tasas históricas' en vez de 'Tasas en vivo'.
Sin sistema de autenticación propio
Para dos usuarios conocidos con correos fijos, construir un sistema de registro, login, JWT y recuperación de contraseña es sobrediseño. Cloudflare Zero Trust resuelve el acceso antes de que la petición llegue a la app, sin una sola línea de código de auth.
Arquitectura
Cómo está construido
La arquitectura prioriza velocidad de entrega y mantenimiento mínimo. Un único contenedor Docker con Next.js, Supabase como backend gestionado y Cloudflare haciendo el trabajo de autenticación.
Next.js 16 App Router: Server Components para el dashboard (force-dynamic, sin caché), Client Components para balance, gastos y estadísticas (interacción, filtros, estado local).
Supabase como backend completo: PostgreSQL con 7 tablas, API REST autogenerada y cliente JS. Las migraciones SQL están versionadas en el repositorio.
Frankfurter API para tipos de cambio en vivo (EUR/THB, EUR/USD, EUR/CHF). Cadena de fallback en tres niveles: API → Supabase cache → valores hardcoded.
effectiveEurAtLiveRate(): función que recalcula el equivalente en euros de cada gasto usando siempre el importe nativo original y las tasas actuales, no las históricas.
Cloudflare Zero Trust como única capa de autenticación. Sin JWT propio, sin tabla de usuarios, sin formulario de login.
Docker + Dokploy en VPS propio. Un solo contenedor. Mismo servidor que otras herramientas del ecosistema.
Modelo de datos
Siete tablas para un viaje completo
El modelo refleja la realidad del viaje: gastos en varias monedas vinculados a países y alojamientos, dos personas con su perfil y presupuesto, y un historial de liquidaciones separado de los gastos.
expensesEl núcleo del sistema. Guarda cuatro columnas de importe (amount_thb, amount_usd, amount_chf, amount_eur) más la divisa original (input_currency) para poder recalcular siempre desde el valor nativo.
settlementsTransferencias reales de dinero entre las dos personas, separadas de los gastos. Permiten liquidar la deuda sin marcar cada gasto individual como saldado.
profilesLos dos viajeros con su nombre, color, emoji y presupuesto total para el viaje. La tabla tiene solo dos filas.
currency_ratesCache de tipos de cambio en Supabase. La API de Frankfurter hace upsert por fecha, así que hay una fila por día con las tasas EUR→THB, EUR→USD y EUR→CHF.
countries · accommodations · categoriesTablas de referencia: países visitados con su divisa local, hoteles con check-in/check-out y precio por noche, y categorías de gasto con emoji y color personalizables.
Stack Tecnológico
Tecnologías utilizadas
¿Tienes un proyecto similar?
Puedo ayudarte a diseñar, construir y desplegar productos digitales claros, seguros y preparados para operar en producción.