Resumen
SpendSync es un producto de gestión de gastos compartidos para personas que viven o gastan juntas. Resuelve dos problemas de dinero distintos en un mismo sistema: cuentas de servicios recurrentes que se repiten cada mes, y gastos puntuales divididos entre un grupo. El producto está desplegado públicamente, tiene un modelo de suscripción de tres niveles cobrado por Stripe, y se opera como un servicio real y no como una demo.
- Niveles de suscripción
- 3Free, Plus a $2.99/mes, Pro a $7.99/mes
- Idiomas de la interfaz
- 2Inglés y español, intercambiables desde el encabezado
- Tablas relacionales
- 15Esquema MySQL documentado en las notas de arquitectura del proyecto
Problema
Dividir dinero con otras personas falla de dos formas distintas, y la mayoría de herramientas solo resuelve una. Una cena puntual es un monto único dividido entre unas cuantas personas que estuvieron de acuerdo en estar ahí. La renta y los servicios son lo opuesto: el monto cambia cada mes, el mismo grupo se repite indefinidamente, y alguien tiene que acordarse de quién ya pagó este ciclo.
Tratar ambos como el mismo objeto produce un esquema que está mal para al menos uno de los dos. Tratarlos como cosas sin relación produce dos productos atornillados entre sí, con dos bandejas de entrada, dos nociones de 'quién me debe' y ninguna vista compartida del mes de un hogar.
Restricciones
- Una sola persona construye y opera todo el sistema, así que el costo de operación tiene que quedarse cerca del precio de un servidor pequeño
- Hay dinero de por medio: un participante nunca debe quedar agregado en silencio a una deuda que no aceptó
- Los usuarios no son técnicos — el flujo de invitación, respuesta y liquidación tiene que ser obvio
- Dos idiomas desde el inicio, porque la audiencia prevista no es solo angloparlante
Rol y contribución
Creador del producto e Ingeniero de Software
- Definí el producto, los niveles de precio y los límites de funcionalidad entre ellos
- Diseñé el esquema relacional que cubre cuentas, participantes, invitaciones, ciclos y amistades
- Construí la API en Express, la capa de autorización y el canal de notificaciones en tiempo real
- Construí el cliente en React, incluida la interfaz internacionalizada
- Integré Amazon Cognito para identidad y Stripe para la facturación por suscripción
- Aprovisiono y opero el entorno de producción en AWS
Diseño del sistema
Arquitectura
Topología de producción de SpendSync
El cliente React, alojado en AWS Amplify, se autentica contra Amazon Cognito y recibe un JWT. Llama a una API en Node y Express sobre HTTPS con un token bearer, y mantiene una conexión Socket.IO abierta al mismo servidor para actualizaciones en vivo. El servidor Express corre en Docker sobre una instancia de AWS Lightsail detrás de Nginx, verifica cada token contra las llaves públicas de Cognito, lee y escribe una base de datos MySQL, envía los correos de invitación por Amazon SES y delega el checkout de la suscripción y su gestión a Stripe.
- Frontend
- React 19
- Vite
- Redux Toolkit + RTK Query
- Radix UI
- Tailwind CSS
- i18next
- Backend
- Node.js
- Express 4
- Socket.IO
- mysql2
- Nodemailer
- Datos
- MySQL
- Esquema relacional de 15 tablas
- Identidad y pagos
- Amazon Cognito
- aws-jwt-verify
- Stripe Checkout
- Portal de facturación de Stripe
- Infraestructura
- AWS Lightsail
- Docker Compose
- Nginx
- AWS Amplify Hosting
- Amazon SES
Usuarios y caso de uso
El producto está dirigido a personas que comparten costos recurrentes: compañeros de piso, parejas, familias y grupos pequeños que ya dividen dinero de manera informal por chat y terminan cada mes reconstruyendo quién pagó qué.
Los niveles de precio siguen esa realidad en vez de una métrica de uso abstracta. El plan gratuito permite transacciones personales ilimitadas pero solo una cuenta compartida activa, que alcanza para llevar tu propio gasto. Plus sube los límites compartidos y agrega exportación y proyección. Pro elimina los límites y agrega la liquidación de entrada y salida — el momento exacto en que un hogar de verdad necesita un balance limpio.
Modelo de dominio
El esquema mantiene los dos problemas de dinero separados a nivel de tablas en lugar de forzar una sola abstracción sobre ambos.
Las cuentas de servicios son el lado recurrente. Una cuenta tiene renglones, participantes con un monto adeudado, invitaciones con estado de aceptada o rechazada, una bitácora de actividad, y una fila por ciclo mensual que registra quién pagó en ese mes específico. Una cuenta pasa por draft, pending_responses y finalized.
Las transacciones son el lado puntual: un registro tipificado (gasto, cuenta o ingreso) con recurrencia opcional, sus propios participantes que cargan tanto un estado de invitación como un estado de pago, y pagos por ciclo para el caso recurrente.
Una tabla aparte de friendships lleva el grafo social, para que una invitación pueda dirigirse a alguien que el usuario ya conoce, y una tabla de invitaciones por correo basada en token cubre a las personas que todavía no tienen cuenta.
- service_bills → service_bill_items, service_bill_participants, bill_invitations, monthly_cycle_payments, bill_activity_log, email_invitations
- transactions → transaction_participants, transaction_cycle_payments
- friendships con estados pendiente, aceptada y bloqueada
Manejo de valores monetarios
Dividir un monto entre personas casi nunca da exacto, y la aritmética de punto flotante lo empeora introduciendo un error que se acumula a lo largo de los ciclos. Por eso el esquema guarda un amount_owed explícito por participante en lugar de guardar un porcentaje y recalcular la parte en cada lectura: la división se decide una vez, se escribe, y nunca se vuelve a derivar.
Eso también vuelve significativa la bitácora. El balance de un participante es la suma de filas que efectivamente se escribieron, así que la bitácora de actividad y el balance siempre coinciden.
Despliegue y operación
La API corre como un contenedor Docker en una instancia de AWS Lightsail detrás de Nginx, que termina TLS y hace proxy al puerto de la aplicación, de modo que el contenedor nunca queda expuesto directamente. El cliente React se compila y se sirve por AWS Amplify Hosting. Identidad, correo y pagos son servicios administrados de AWS y Stripe en lugar de componentes autoalojados.
La forma de esa decisión importa más que los servicios específicos: las partes que son caras de equivocar — identidad, entregabilidad de correo, manejo de tarjetas — están delegadas, y la parte que carga la lógica real del producto es la parte que se opera en casa.
Monetización
SpendSync se vende como suscripción con un nivel gratuito. La compra pasa por Stripe Checkout, y a los suscriptores existentes se les envía al portal de facturación de Stripe para cambiar o cancelar su plan, de manera que la aplicación nunca maneja datos de tarjeta ni construye sus propias pantallas de facturación.
Los límites entre niveles están trazados donde el producto cuesta dinero operarlo y donde empieza a crear valor real para un hogar: cuentas compartidas, amigos, profundidad de historial y exportación.
Decisiones clave y trade-offs
Decisión
Modelar las cuentas recurrentes y los gastos puntuales como dos sistemas separados
- Contexto
- Una cuenta mensual de servicios y una cena compartida se ven parecidas en la interfaz pero se comportan distinto: una se repite con monto cambiante y grupo estable, la otra ocurre una vez con un grupo improvisado.
- Alternativas consideradas
- Una sola tabla genérica de 'gasto' con un campo de recurrencia anulable
- Dos productos completamente separados que solo comparten el inicio de sesión
- Enfoque elegido
- Dos sistemas de primera clase — cuentas de servicios y transacciones — que comparten identidad, el grafo de amistades y el canal de notificaciones.
- Razón
- Cada sistema obtiene un esquema que le queda. Las cuentas recurrentes tienen filas de pago a nivel de ciclo; los gastos puntuales tienen liquidación a nivel de participante sin cargar maquinaria de ciclos que no usan.
- Trade-off
- Dos rutas de código que mantener, y algunos conceptos de interfaz aparecen dos veces. El costo es superficie duplicada a cambio de consultas que se mantienen simples en ambos casos.
Decisión
Delegar la identidad a Amazon Cognito en lugar de construir la autenticación
- Contexto
- El producto maneja dinero entre personas, y lo construye y opera una sola persona.
- Alternativas consideradas
- Correo y contraseña locales con hashing, recuperación y manejo de sesión propios
- Un producto de identidad de terceros con precio por usuario
- Enfoque elegido
- Amazon Cognito en el cliente vía Amplify, con verificación en el servidor contra las llaves públicas de Cognito en cada ruta protegida.
- Razón
- El almacenamiento de contraseñas, los flujos de recuperación y la rotación de tokens son exactamente el tipo de trabajo donde una implementación de un solo mantenedor tiene más probabilidad de estar silenciosamente mal, y no es donde el producto se diferencia.
- Trade-off
- Una dependencia dura de un proveedor de nube para el inicio de sesión, y un registro de usuario que hay que sincronizar hacia la base de datos de la aplicación en el primer login.
- Resultado
- La base de datos de la aplicación no guarda ninguna credencial; la fila local del usuario está indexada por el sujeto del proveedor de identidad.
Decisión
Exigir una respuesta explícita antes de que un participante deba algo
- Contexto
- La implementación más barata es dejar que un dueño agregue a cualquiera a una cuenta y mostrarle la deuda de inmediato.
- Alternativa considerada
- Agregar participantes directamente, con la opción de removerse después
- Enfoque elegido
- Todo participante empieza en estado de invitación pendiente y solo carga un monto cuando acepta.
- Razón
- Un número que alguien no aceptó no es una deuda, es un reclamo. Hacer del consentimiento un estado del esquema mantiene esa distinción exigible en vez de que sea una convención de la interfaz.
- Trade-off
- Un viaje extra antes de que una cuenta quede liquidada, y un dueño puede quedar bloqueado por un participante que nunca responde — que es justamente la razón por la que las cuentas se pueden finalizar y reabrir.
Decisión
Correr el canal de tiempo real en el mismo servidor que la API HTTP
- Contexto
- Invitaciones, respuestas y pagos necesitan llegar a las pantallas de otras personas sin recargar.
- Alternativas consideradas
- Sondeo desde el cliente cada cierto intervalo
- Un servicio de tiempo real aparte o un producto administrado de pub/sub
- Enfoque elegido
- Socket.IO conectado al mismo servidor HTTP que sirve la API de Express.
- Razón
- A esta escala el volumen de notificaciones es diminuto y lo generan acciones de usuario que la API ya maneja. Compartir proceso mantiene el despliegue en un solo contenedor y permite emitir el evento en el mismo lugar donde se escribe el cambio de estado.
- Trade-off
- La API y la capa de sockets escalan juntas y no se pueden dimensionar por separado. Separarlas es un problema posterior, no un problema de lanzamiento.
Lo más difícil: hacer que una cuenta finalizada sobreviva a la vida real
El trabajo difícil no fue dividir un monto. Fue el hecho de que una cuenta compartida nunca está terminada cuando el software cree que lo está. Un ciclo se cierra y luego llega un pago tardío. Alguien acepta una invitación dos semanas después que el resto del grupo. Un participante paga una parte de lo que debe y el resto el mes siguiente. Alguien se muda a mitad de ciclo.
Cada una de esas es una transición de estado que tiene que ser legal sin corromper el historial que la precede. Por eso el esquema lleva el pago con granularidad de ciclo en vez de como un booleano en el participante, por eso las cuentas se pueden reabrir y no solo finalizar, y por eso la bitácora de actividad es una tabla y no una vista derivada: una vez que una cuenta puede moverse hacia atrás, el único registro confiable de lo que pasó es el que se escribió en el momento.



Resultado
- Desplegado públicamente y operando en app.spend-sync.com con un embudo de suscripción autogestionado
- Un modelo comercial de tres niveles cuyos límites corresponden a las partes del sistema que realmente cuestan dinero operar
- Ciclo de vida completo manejado en producción: invitación, aceptación, ciclos mensuales, liquidación parcial y reapertura
- Interfaz bilingüe que alcanza usuarios fuera de una audiencia solo en inglés
Aprendizajes
- La parte difícil de un producto de dinero es el desacuerdo, no la aritmética. El consentimiento y la reversibilidad merecen soporte a nivel de esquema, no un diálogo de confirmación.
- Delegar identidad y pagos fue la decisión de mayor apalancamiento disponible para un solo mantenedor: eliminó los dos modos de falla con peores consecuencias.
- El precio es una restricción de diseño. Decidir qué pertenece al nivel gratuito obligó a una respuesta mucho más clara sobre qué parte del producto es realmente valiosa.
- Escribir la división le gana a recalcularla. El dinero derivado cambia cuando cambia el código; el dinero almacenado no.
Próximas mejoras
- Cobertura de pruebas automatizadas sobre la máquina de estados de ciclos y liquidación, donde las reglas son más intrincadas
- Reporte estructurado de errores y monitoreo de disponibilidad, hoy más delgados que el resto del sistema
- Separar el canal de tiempo real de la API HTTP cuando las conexiones concurrentes justifiquen escalarlos aparte
- Instrumentar el embudo de registro gratuito a conversión de pago, para que las decisiones de precio se puedan medir en vez de discutir
