All projects
SimplyBudget
SimplyBudget screenshot 1
Ref
P-02
Period
2025 - 2026
Status
Live
Role
Build

A personal finance app for money that repeats. You describe each commitment once (what it is, how much, how often, from when) and the app projects it across the weeks, months and years ahead, then records what actually happened against it.

The problem

Budgeting tools mostly categorise the past: import a statement, sort the rows, learn in arrears where the money went. For commitments that are already fixed, like a salary, rent and standing orders, that work is redundant, because the amounts were knowable in advance. The useful question is whether the period matched the plan: did the direct debit leave at the figure you expected, and how many bills are still outstanding with a week of the month left.

What I built

Four screens over one model. My Budget is the editor: a grid of commitments (description, amount, cadence, start date) with search, filters, sorting and a single save that works out what to create, update and delete. Tracker turns the current week, month or year into a checklist, ticking items off as paid or received and correcting the amount when it differed. Reports sets expected against actual for that period, item by item, with the totals and the difference. Dashboard annualises it into income, outgoings, net and savings rate. Around all of it: email-verified accounts, self-service credential changes, five accent themes with a light mode, and a choice of currency.

Architecture

Django REST Framework over Postgres, with a Vue single-page app on a separate origin. Two models carry the whole app: a BudgetItem is a standing commitment (type, amount, cadence, start date) and a Transaction is one occurrence of it on one date, holding what actually moved and whether it cleared. The API owns every piece of date arithmetic. A request names a type, a cadence and any date inside the period; the server resolves the boundaries itself, creates whichever occurrences that period does not have yet, and returns them joined to their plan. Auth is session cookies with CSRF. Redis holds pending signups, so Postgres never holds anything unverified.

Stack

Django + DRFSessions, password hashing, throttling and permissions are the parts of a money app worst spent hand-rolling. Everything user-owned is scoped by request.user in one place.
Vue.jsEvery screen is the same data under a different reading, and the editor is a grid of live rows carrying unsaved state. Computed values over a reactive store say that directly.
PostgreSQLA commitment and its occurrences are relational, and one occurrence per item per date is a constraint the database should hold rather than the application.
RedisVerification codes have to expire on their own and be readable by every worker process. A key with a TTL is exactly that.

Decisions

  1. 01Split the plan from what happened, and create the occurrences only when a period is first opened. A commitment is one row; an occurrence is one date, one actual amount, one paid flag. Nothing is written for months nobody looks at, and editing a commitment never rewrites history. The cost is that a period you never open has no record: fine for something checked weekly, wrong for an audit trail.
  2. 02No user row exists until the emailed code is verified. Signup puts the pending details and a SHA-256 hash of a six-digit code into Redis for fifteen minutes, and the account is created inside the verification request rather than before it. An abandoned signup expires on its own instead of leaving an unverified row for someone to collide with or claim.
  3. 03Made signup answer identically whether or not the address is already registered. A different message for a taken email turns the form into a membership oracle for any address someone cares to test. The existing account simply receives no mail.
  4. 04Put two independent brakes on login. django-axes locks a username and IP pair out for an hour after ten failures, and underneath it each failed attempt adds an exponential delay, capped at eight seconds. The lockout stops a sustained attack; the delay makes the cheap, fast part of one uneconomic long before it.
  5. 05Kept money as a fixed-point decimal to two places, from the column through to the input field, with the same floor, precision and digit limit enforced on both sides of the wire. A client that rounds differently from the server produces a figure that looks saved and is not.
  6. 06Wrote the three charts rather than installing one. A donut of SVG arcs, paired bars for expected against actual, and a month meter marked with today's pace. They read the same custom properties as the rest of the app, which makes a sixth theme a variable change instead of a chart config.
  7. 07Held the app behind a loader that retries its first request five times. The API sleeps when idle on its hosting tier, so the first visitor of the morning waits on a cold start, and a blank page would read as a broken site. The loader names the attempt it is on.

Outcome

Live at simplybudget.co.uk with open signup, built over about ten months. The split between a commitment and its occurrences was the load-bearing decision: Dashboard and Reports were both added near the end and needed no schema change, because each is only a different reading of the pair the tracker was already writing.

Tagged

JavaScriptPythonVue.jsDjangoPostgreSQL