Saltar al contenido principal
Todos los casos de estudio

SaaS monetizado · Gestión de gastos compartidos

SpendSync

Un SaaS por suscripción para hogares compartidos: cuentas recurrentes, gastos divididos, invitaciones y liquidación, corriendo en producción sobre AWS.

En vivo · Producto por suscripciónGenera ingresos
Rol
Creador del producto e Ingeniero de Software
Periodo
2025 — presente

Etapas cubiertas

  • Idea
  • Construcción
  • Despliegue
  • Operación
  • Monetización
app.spend-sync.com
Página principal de SpendSync en escritorio, con el titular 'Split bills effortlessly with friends' y las acciones Get Started y Learn more.
app.spend-sync.com — página pública de presentación

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

Seguridad y autorización

La identidad está delegada a Amazon Cognito. El cliente inicia sesión con Amplify y recibe un JWT; cada ruta protegida del servidor verifica ese token contra las llaves públicas de Cognito antes de que corra un handler, y el sujeto verificado se convierte en la identidad de la petición. La base de datos de la aplicación nunca almacena una contraseña.

La regla que de verdad importa no es la autenticación sino el consentimiento. La fila de un participante existe en estado de invitación pending hasta que la persona invitada responde explícitamente. Los montos adeudados solo son reales una vez aceptados. La propiedad es explícita tanto en cuentas como en transacciones, así que las operaciones destructivas — finalizar, reabrir, iniciar un nuevo ciclo — se validan contra el dueño y no contra la membresía.

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.

app.spend-sync.com
Sección de precios de SpendSync mostrando tres niveles: Free a cero dólares, Plus a 2.99 dólares al mes y Pro a 7.99 dólares al mes, cada uno con su lista de funciones.
Modelo de suscripción de tres niveles con alternador mensual y anual
app.spend-sync.com
Sección de funciones de SpendSync con cuatro tarjetas: Bill Tracking, Fair Splits, Calendar View y Real-time Alerts.
Conjunto de funciones tal como se presenta a los usuarios potenciales
Página principal de SpendSync renderizada en una pantalla móvil, con el titular apilado y la llamada a la acción principal.
Vista móvil

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