Skip to main content
All case studies

Monetized SaaS · Shared Expense Management

SpendSync

A subscription SaaS for shared households: recurring bills, split expenses, invitations and settlement, running in production on AWS.

Live · Subscription productRevenue-generating
Role
Product creator and Software Engineer
Period
2025 — present

Stages covered

  • Idea
  • Build
  • Deploy
  • Operate
  • Monetise
app.spend-sync.com
SpendSync landing page on desktop, headline reading 'Split bills effortlessly with friends' with Get Started and Learn more actions.
app.spend-sync.com — public landing page

Overview

SpendSync is a shared-expense management product for people who live or spend together. It handles two different money problems in one system: recurring service bills that repeat every month, and one-off expenses split across a group. The product is publicly deployed, has a three-tier subscription model billed through Stripe, and is operated as a real service rather than a demo.

Subscription tiers
3Free, Plus at $2.99/mo, Pro at $7.99/mo
Interface languages
2English and Spanish, switchable in the header
Relational tables
15MySQL schema documented in the project's architecture notes

Problem

Splitting money with other people fails in two different ways, and most tools only solve one of them. A one-off dinner is a single amount divided between a handful of people who all agreed to be there. Rent and utilities are the opposite: the amount changes every month, the same group repeats indefinitely, and somebody has to remember who has already paid this cycle.

Treating both as the same object produces a schema that is wrong for at least one of them. Treating them as unrelated produces two products bolted together, with two inboxes, two notions of 'who owes me', and no shared view of a household's month.

Constraints

  • One person builds and operates the whole system, so the running cost has to stay near the price of a single small server
  • Money is involved: a participant must never be silently added to a debt they did not accept
  • Users are non-technical — the invitation, response and settlement flow has to be obvious
  • Two languages from the start, because the intended audience is not English-only

Role and contribution

Product creator and Software Engineer

  • Defined the product, the pricing tiers and the feature boundaries between them
  • Designed the relational schema covering bills, participants, invitations, cycles and friendships
  • Built the Express API, the authorisation layer and the real-time notification channel
  • Built the React client, including the internationalised interface
  • Integrated Amazon Cognito for identity and Stripe for subscription billing
  • Provisioned and operates the production environment on AWS

System design

Architecture

SpendSync production topology

The React client, hosted on AWS Amplify, authenticates against Amazon Cognito and receives a JWT. It calls a Node and Express API over HTTPS with a bearer token, and holds an open Socket.IO connection to the same server for live updates. The Express server runs in Docker on an AWS Lightsail instance behind Nginx, verifies every token against the Cognito public keys, reads and writes a MySQL database, sends invitation email through Amazon SES, and delegates subscription checkout and billing management to Stripe.

Frontend
  • React 19
  • Vite
  • Redux Toolkit + RTK Query
  • Radix UI
  • Tailwind CSS
  • i18next
Backend
  • Node.js
  • Express 4
  • Socket.IO
  • mysql2
  • Nodemailer
Data
  • MySQL
  • 15-table relational schema
Identity & payments
  • Amazon Cognito
  • aws-jwt-verify
  • Stripe Checkout
  • Stripe billing portal
Infrastructure
  • AWS Lightsail
  • Docker Compose
  • Nginx
  • AWS Amplify Hosting
  • Amazon SES

Users and use case

The product is aimed at people who share recurring costs: roommates, couples, families and small groups who already split money informally over chat and end each month reconstructing who paid what.

The pricing tiers follow that reality rather than an abstract usage metric. The free tier allows unlimited personal transactions but only one active shared bill, which is enough to track your own spending. Plus raises the shared limits and adds export and forecasting. Pro removes the limits and adds move-in and move-out settlement — the specific moment when a household actually needs a clean balance.

Domain model

The schema keeps the two money problems separate at the table level instead of forcing one abstraction over both.

Service bills are the recurring side. A bill owns line items, participants with an amount owed, invitations with an accepted or rejected state, an activity log, and a row per monthly cycle recording who has paid in that specific month. A bill moves through draft, pending_responses and finalized.

Transactions are the one-off side: a typed record (expense, bill or income) with an optional recurrence, its own participants each carrying both an invitation status and a payment status, and cycle payments for the recurring case.

A separate friendships table carries the social graph, so an invitation can address someone the user already knows, and a token-based email invitation table covers people who do not have an account yet.

  • 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 with pending, accepted and blocked states

Security and authorisation

Identity is delegated to Amazon Cognito. The client signs in through Amplify and receives a JWT; every protected route on the server verifies that token against the Cognito public keys before a handler runs, and the verified subject becomes the user identity for the request. The application database never stores a password.

The consequential rule is not authentication but consent. A participant row exists in a pending invitation state until the invited person explicitly responds. Amounts owed are only real once accepted. Ownership is explicit on both bills and transactions, so destructive operations — finalising, reopening, starting a new cycle — are checked against the owner rather than against membership.

Handling monetary values

Splitting an amount between people almost never divides evenly, and floating-point arithmetic makes that worse by introducing error that accumulates across cycles. The schema therefore stores an explicit amount_owed per participant rather than storing a percentage and recomputing the share on every read: the split is decided once, written down, and never re-derived.

That also makes the audit trail meaningful. A participant's balance is the sum of rows that were actually written, so the activity log and the balance always agree.

Deployment and operations

The API runs as a Docker container on an AWS Lightsail instance behind Nginx, which terminates TLS and proxies to the application port so the container is never directly exposed. The React client is built and served through AWS Amplify Hosting. Identity, email and payments are managed AWS and Stripe services rather than self-hosted components.

The shape of that choice matters more than the specific services: the parts that are expensive to get wrong — identity, mail deliverability, card handling — are delegated, and the part that carries the product's actual logic is the part that is self-operated.

Monetisation

SpendSync is sold as a subscription with a free tier. Purchase runs through Stripe Checkout, and existing subscribers are sent to the Stripe billing portal to change or cancel their plan, so the application never handles card details or builds its own billing screens.

The tier boundaries are drawn where the product costs money to run and where it starts creating real value for a household: shared bills, friends, history depth and export.

Key decisions and trade-offs

Decision

Model recurring bills and one-off expenses as two separate systems

Context
A monthly utility bill and a shared dinner look similar in the interface but behave differently: one repeats with a changing amount and a stable group, the other happens once with an ad-hoc group.
Alternatives considered
  • A single generic 'expense' table with a nullable recurrence field
  • Two entirely separate products sharing only a login
Chosen approach
Two first-class systems — service bills and transactions — sharing identity, the friendship graph and the notification channel.
Reason
Each system gets a schema that fits it. Recurring bills get cycle-level payment rows; one-off expenses get participant-level settlement without carrying unused cycle machinery.
Trade-off
Two code paths to maintain, and some UI concepts appear twice. The cost is duplicated surface area in exchange for queries that stay simple in both cases.

Decision

Delegate identity to Amazon Cognito instead of building authentication

Context
The product handles money between people, and it is built and operated by one person.
Alternatives considered
  • Local email and password with self-managed hashing, reset and session handling
  • A third-party identity product with per-user pricing
Chosen approach
Amazon Cognito on the client through Amplify, with server-side verification against the Cognito public keys on every protected route.
Reason
Password storage, reset flows and token rotation are exactly the kind of work where a single-maintainer implementation is most likely to be quietly wrong, and it is not where the product is differentiated.
Trade-off
A hard dependency on one cloud provider for sign-in, and a user record that has to be synchronised into the application database on first login.
Outcome
The application database stores no credentials at all; the local user row is keyed by the identity provider's subject.

Decision

Require an explicit response before a participant owes anything

Context
The cheapest implementation is to let an owner add anyone to a bill and show them the debt immediately.
Alternative considered
  • Add participants directly, with the option to remove themselves afterwards
Chosen approach
Every participant starts in a pending invitation state and only carries an amount once they accept.
Reason
A number someone did not agree to is not a debt, it is a claim. Making consent a state in the schema keeps that distinction enforceable rather than a UI convention.
Trade-off
An extra round trip before a bill is settled, and an owner can be blocked by a participant who never responds — which is why bills can be finalised and reopened.

Decision

Run the real-time channel on the same server as the HTTP API

Context
Invitations, responses and payments all need to reach other people's screens without a refresh.
Alternatives considered
  • Client polling on an interval
  • A separate real-time service or a managed pub/sub product
Chosen approach
Socket.IO attached to the same HTTP server that serves the Express API.
Reason
At this scale the notification volume is tiny and driven by user actions the API already handles. Sharing a process keeps the deployment to one container and lets an event be emitted in the same place the state change is written.
Trade-off
The API and the socket layer scale together and cannot be sized independently. Splitting them is a later problem, not a launch problem.

The hardest part: making a finalised bill survive real life

The difficult work was not splitting an amount. It was the fact that a shared bill is never finished when the software thinks it is. A cycle is closed and then a late payment arrives. Someone accepts an invitation two weeks after the rest of the group. A participant pays part of what they owe and the rest next month. Someone moves out mid-cycle.

Each of those is a state transition that has to be legal without corrupting the history that came before it. That is why the schema carries payment at cycle granularity rather than as a single boolean on the participant, why bills can be reopened rather than only finalised, and why the activity log is a table instead of a derived view: once a bill can move backwards, the only trustworthy record of what happened is the one that was written at the time.

app.spend-sync.com
SpendSync pricing section showing three tiers: Free at zero dollars, Plus at 2.99 dollars per month, and Pro at 7.99 dollars per month, each with a feature list.
Three-tier subscription model with a monthly and yearly toggle
app.spend-sync.com
SpendSync features section with four cards: Bill Tracking, Fair Splits, Calendar View and Real-time Alerts.
Feature set as presented to prospective users
SpendSync landing page rendered on a mobile viewport, showing the stacked headline and primary call to action.
Mobile viewport

Result

  • Publicly deployed and operating at app.spend-sync.com with a self-service subscription funnel
  • A three-tier commercial model whose limits map to the parts of the system that actually cost money to run
  • Complete lifecycle handled in production: invitation, acceptance, monthly cycles, partial settlement and reopening
  • Bilingual interface reaching users outside an English-only audience

Lessons learned

  • The hard part of a money product is disagreement, not arithmetic. Consent and reversibility deserve schema-level support, not a confirmation dialog.
  • Delegating identity and payments was the highest-leverage decision available to a single maintainer — it removed the two failure modes with the worst consequences.
  • Pricing is a design constraint. Deciding what belongs to the free tier forced a much clearer answer about which part of the product is actually valuable.
  • Writing the split down beats recomputing it. Derived money changes when the code changes; stored money does not.

Next improvements

  • Automated test coverage over the cycle and settlement state machine, where the rules are most intricate
  • Structured error reporting and uptime monitoring, currently thinner than the rest of the system
  • Splitting the real-time channel from the HTTP API once concurrent connections justify separate scaling
  • Instrumenting the funnel from free signup to paid conversion, so pricing decisions can be measured rather than argued