Saltar al contenido
Volver a proyectos

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

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.

4
divisas nativas: EUR, THB, USD, CHF
7
tablas en Supabase
Live
balance recalculado al tipo de cambio actual vía Frankfurter API

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.

01

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.

02

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.

03

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.

04

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.

05

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.

06

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.

1

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).

2

Supabase como backend completo: PostgreSQL con 7 tablas, API REST autogenerada y cliente JS. Las migraciones SQL están versionadas en el repositorio.

3

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.

4

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.

5

Cloudflare Zero Trust como única capa de autenticación. Sin JWT propio, sin tabla de usuarios, sin formulario de login.

6

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.

expenses

El 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.

datecountry_idaccommodation_idcategory_idamount_thb / usd / chf / eurinput_currencypaid_by (profile_id)expense_for (shared|persona_a|persona_b)is_settled
settlements

Transferencias reales de dinero entre las dos personas, separadas de los gastos. Permiten liquidar la deuda sin marcar cada gasto individual como saldado.

from_profileto_profileamount_eurdatenotes
profiles

Los dos viajeros con su nombre, color, emoji y presupuesto total para el viaje. La tabla tiene solo dos filas.

display_namecoloremojitotal_budget
currency_rates

Cache 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.

date (PK)eur_to_thbeur_to_usdeur_to_chfsource (live|fallback)
countries · accommodations · categories

Tablas 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.

flag_emojicurrency_codeexchange_ratecheck_in / check_outprice_per_nighticoncolor

Stack Tecnológico

Tecnologías utilizadas

Next.js 16React 19TypeScriptSupabasePostgreSQLTailwind CSS 4RechartsFrankfurter APICloudflare Zero TrustDokployDocker

¿Tienes un proyecto similar?

Puedo ayudarte a diseñar, construir y desplegar productos digitales claros, seguros y preparados para operar en producción.