Skip to content

Author

Ali Arbab

Project

02 / SpendInCheck

Status

Live — open demo

Navigate

← projects

§ 01

Pitch

SpendInCheck

A personal finance tracker that tells you whether you're on budget, not just what you spent.

Started as my Class XII Computer Science practical — Python, raw parameterised SQL, one operations module shared by a console app and a web app — then rebuilt into a product: a React + TypeScript client over a Flask JSON API on Postgres. Budgets against actuals, a ledger with accounts, transfers and recurring rules, and a portfolio priced live from StockSaathi's public API. Open demo, no sign-up.

  • 8Reports
  • 174msOpen the demo (was 3,100)
  • 42msA year of reports (was 1,496)
  • 17Ordered SQL migrations

As of

§ 02

Access

§ 03

Origin

From a practical file to a product.

SpendInCheck began as my Class XII Computer Science practical. The rules were strict: Python, a real SQL database, every query written out by hand with %s placeholders, and code I could defend line by line in a viva. The one decision that mattered most came from that brief — every query lives in a single operations module that returns plain data and never prints. A console app and a web app both called the same functions, with zero duplicated SQL.

After the practical was submitted I kept going. The server-rendered pages were deleted and replaced with a React + TypeScript client over a Flask JSON API, and the database moved to PostgreSQL on Supabase. Because the SQL already lived in one place, rebuilding the entire frontend needed no new queries at all.

§ 04

Reports

Eight questions a list of transactions can't answer.

  • Category-wise spendWhere did the money actually go this month?
  • Budget vs actualWhich categories blew past their limit, and by how much?
  • Portfolio P&LWhich holdings are up, which are down, what's the net?
  • Net worth over timeWhat am I worth, month by month, at the prices of the time?
  • Income and expenseWhat came in against what went out, over the last year?
  • Cashflow running totalIs the gap between them widening or closing?
  • Against the marketDid my holdings beat the index, with quantities held constant?
  • Where it goesWhich payees take the most, and how often?

§ 05

Architecture

All SQL in one place, and nothing that runs twice.

  • One module owns the database. Routes call a function and present whatever comes back. The package is split by domain but re-exports every name, so callers still write operations.add_transaction(...).
  • The server answers the API and nothing else. The site is a built bundle served from the edge, split per route, so a page load never wakes Python — only the data it asks for does.
  • Three scheduled jobs, each safe to run twice. A nightly close-price snapshot, a recurring-transaction sweep and a clean-up of abandoned demo accounts. The sweep advances a rule and writes its row in one transaction, with a unique index making a repeat a no-op rather than a double entry.
  • Transfers are two rows sharing a group. Money moving between your own accounts is neither income nor spending, and every report knows it.
  • Tests run against a real Postgres. A query that forgets its user_id filter looks correct in Python and only misbehaves in the database, so a mocked cursor would pass exactly the bug worth catching. The suite also asserts one account can never reach another's rows.
  • The database is closed to everything but the app. Supabase exposes every public table to its publishable key by default. Row-level security is on for all of them with no policies, and the default grants are revoked so a future table can't reopen it.

§ 06

Integration

Priced by StockSaathi, my other product.

Point a holding at an NSE symbol and SpendInCheck fetches its price instead of asking you to type one. Every quote, fund NAV and instrument name comes from StockSaathi's public API and its published instrument universe — two products of mine talking to each other, rather than both scraping the same upstream.

  • The two feeds disagree on units: live quotes arrive in rupees, history in paise. One service owns that conversion so the rest of the app never sees it.
  • A nightly job records each tracked symbol's close, so net worth for a past month is valued at what things were actually worth then, not at today's price.
  • “Against the market” compares the portfolio with an index ETF at today's quantities in every month — otherwise a month of heavy saving reads as spectacular returns.

§ 07

Performance

Measured, not asserted.

Every figure on this page came from a benchmark script, not from reasoning about the code. Against the database pooler, opening a connection costs about 182ms and each round trip about 28ms whatever it carries — so the SQL itself is noise, and making a screen fast means counting statements. Connections are pooled and kept warm, and every read endpoint is one round trip.

The pooler also taught me not to trust a number I hadn't measured: Postgres reports a limit of sixty connections, but the pooler in front of it hands out sixteen, and the seventeenth connects fine and then dies on first use.

§ 08

Stack

  • React 19 + TypeScript + Vite
  • TanStack Query + React Router
  • Flask JSON API
  • PostgreSQL (Supabase), raw SQL
  • psycopg2 connection pool
  • pytest against a real database
  • StockSaathi public API for prices

Started · page reviewed

↑↓ navigate↵ selectESC close
15 results
SpendInCheck — on budget, not just spent — Ali Arbab — Ali Arbab