All projects

Spendly

A personal finance web app for freelancers and people with irregular income — log an entry in seconds, Locket-style, and answer “how many days will my money last?” instead of being forced into a monthly budget. Works well on both phone and desktop.

Date05/2026 - nowTech
  • React 19
  • Vite
  • TanStack Router
  • TanStack Query
  • Tailwind CSS 4
  • PWA
  • Astro
  • NestJS
  • Prisma
  • PostgreSQL
  • Redis
  • BullMQ
  • Zod
  • AWS S3
  • Web Push
  • payOS
Spendly

Highlights

  • Log income or spending in seconds: snap a photo, enter the amount, done — as quick as posting a Locket.
  • Income is spendable right away — no “moving money between wallets” and no planning required first.
  • Forgot to log for a few days? No problem — back-log several entries at once, with the right day and time.
  • See how many days your money will last, based on your own spending habits.
  • Set a goal if you want: an income goal (how much you've earned, when you'll hit it) or a survival goal (how much to spend per day). Skipping it is fine too.
  • No judgment: overspending only changes a gentle warning color and never stops you from logging.
  • A photo journal grouped by day, plus spending statistics by category.
  • Gentle reminders via notifications, an in-app inbox and email when you haven't logged in a while or money is running low.
  • Smooth on both phone and desktop, and installable on your home screen like an app.

About the project

Spendly is a personal project I've been building since May 2026 and am still developing; the landing page is at spendly.liqwy.com and the app at spendly-app.liqwy.com.

The problem comes from people with irregular income: some months are high, some are close to zero — for them “how much should I spend this month?” is meaningless. The right question is: “how many days will the money I have last?”. Spendly answers it from what users log every day.

The first version split money into 3 wallets: new income had to sit in a savings wallet and be moved manually before it could be spent, plus daily streaks to keep discipline. Digging deeper, I realized Spendly is a logging app, not a plan-enforcing app — so once planning became optional, those barriers had no reason to exist. Now there's a single real-money wallet, and income is spendable right away.

Goals aren't forced on day one either, because new users don't yet have the data to know how much they should spend. The app only invites them once it can say something concrete, e.g. “Over the past two weeks you spent about 180,000đ a day. With the money you have, you'll last about 47 days.”

Spendly started out phone-only; the interface now works well on desktop too, with Free and PRO plans.

Under the hood

  • The frontend is a monorepo with a Vite + React 19 + TanStack Router/Query app (PWA, responsive) and a static Astro landing page. The backend is NestJS + Prisma + PostgreSQL.
  • Money is stored as decimals instead of floats, sent over the API as strings, and every business date uses Vietnam time — because wrong money doesn't throw errors, it silently drifts.
  • Every balance change runs inside one DB transaction and is written to an immutable ledger — a PostgreSQL trigger blocks updates and deletes on ledger rows.
  • A nightly job checks the invariant “balance = total income − total expenses” and raises an alert on drift instead of waiting for users to notice.
  • Images upload straight to S3-compatible storage via presigned URLs, never through the server; the bucket is private and read through short-lived presigned GETs.
  • A separate BullMQ worker handles logging reminders, runway alerts, target reminders and periodic cleanup; notifications go out on 3 channels: Web Push, in-app inbox and email.
  • Auth uses short-lived access tokens + refresh tokens stored in the DB so they can be revoked; Role–Permission access control matches on path + method and denies by default.
  • Redis handles caching and rate limiting shared across instances; if Redis goes down, the API keeps serving instead of returning 500s.
  • Zod schemas are the source of truth for the API contract — the frontend parses responses with zod to catch shape drift between backend and frontend.
  • PRO plan payments go through payOS.

Got questions?

I'm always excited to collaborate on innovative and exciting projects!