SimpleBank

A full banking application — REST API, immutable ledger, admin console — built by a five-person team and deployed on AWS.

Money is an integer number of cents, the ledger only ever appends, and no balance is ever computed in the browser. Most of what makes a banking app trustworthy is a handful of rules you decide once and then refuse to break.

Role
Project manager & front-end lead
Team
5 people
Date
September 2026
Backend
Python standard library only
API
19 routes, token-authenticated
Deployment
AWS — CloudFront, S3, DocumentDB

Live at d1ws2llwooqj6z.cloudfront.net · source on GitHub. The palette borrows from a well-known bank's colours; none of its logo, wordmark or name appears anywhere, which is a deliberate line rather than an oversight.

What it does

Users register, open accounts, deposit, withdraw, transfer money to another person by name, and read a paginated transaction history. Admins freeze and unfreeze accounts with a recorded reason, post correcting ledger entries, and read an audit log of who did what to which account and why. Admins hold no accounts of their own — the separation is enforced in the service layer, not just hidden in the interface.

The backend runs on the Python standard library. No framework, no third-party package, nothing to install. That is a deliberate constraint: it keeps the whole system installable and gradeable on any machine with Python on it, and it forced every routing, auth and serialisation decision to be made explicitly rather than inherited from a framework's defaults.

What I did

The rules that shape everything

The architecture is strictly layered — controller, serialisers, services, models, repository — and each layer depends only on the one below it. The rule is enforced rather than asserted: the business-rule test suite never imports the HTTP layer at all, so if a rule leaked upward into a route handler the tests could not reach it.

Money is an integer number of cents

123456 is £1,234.56. The API rejects 25.00 and "2500" with a 400 rather than guessing, because guessing wrong on a decimal point is a hundredfold error. Dollars are parsed on the way in and formatted on the way out; nothing in between ever holds a float.

The ledger only appends

There is no route that sets a balance, and there should never be one. A correction is a new entry in the opposite direction. An admin reconciliation endpoint lists any account where the balance disagrees with the sum of its ledger, which is the running proof that the rule still holds.

The browser never computes a balance

Every write endpoint returns the whole updated account alongside the transaction, and the client renders that. A balance derived on the client is a balance that can silently disagree with the server.

A double-clicked button cannot move money twice

Deposits, withdrawals and transfers carry a client-generated transaction id, one per submission attempt, so a retry or an impatient second click resolves to the same transaction rather than a second one.

Never authorise off the token's role claim

The token is signed, so it is not forged — but tokens last a week, and a user demoted on Tuesday still carries one saying ADMIN until the following Monday. Authentication re-reads the user from storage and authorises on what the record says now.

Also rejected: exposing account numbers in the interface. A transfer names a person and the server resolves the account; an account belonging to someone else reads as 404, not 403, so the API does not confirm that it exists.

Deployment on AWS

The application was taken from a local demo to a production deployment on AWS, in three pieces:

The design system

This is the part I owned outright, and the part I would point at first. A banking interface is almost entirely form fields, so the contrast that matters is not only body text — it is the border of everything you can click or type into.

Two brand tokens, not one

The brand navy exists twice: once as a fill that white text sits on, and once as ink for headings. A colour bright enough to work as a button almost never clears 4.5:1 as body text, and in dark mode the two move in opposite directions — the fill stays dark enough for white text while only the ink gets lighter. One token doing both jobs is what makes a dark mode unreadable.

Every ratio computed, not eyeballed

All 85 colour tokens carry their measured contrast ratio in a comment beside them — 93 annotations in total. Interactive borders are held to the 3:1 required for non-text contrast, and they are a separate token from the decorative dividers, which have no such requirement and would otherwise drag the interactive ones down with them.

Dark mode has three states, not two

Light, dark, and nothing stored — which means follow the operating system. Choosing the mode your system already asks for clears the saved value rather than pinning it, so the site keeps tracking the system if it changes later. Pinning on first press is the common bug: the site stops following the system forever and appears broken when the machine switches to dark at sunset.

Rejected: inverting the palette. The navy becomes cyan in dark mode, chosen rather than lightened, and the surfaces, hero and field borders each get their own second answer.

Screenshots

Every shot exists in both modes. Flip one with its own switch, or flip all of them with the switch at the top of the page.

SimpleBank home page: a credit card promotion beside the sign-in form
Home and sign in
Sign-in page with email and password fields and a show-password control
Sign in
Registration form with name, email, password and an optional staff code
Register
Welcome screen for a new user who has not opened an account yet
First visit, no accounts
Open account form with account type and opening balance
Open an account
Account list showing checking, savings and credit balances
Your accounts
Detail view for one account with owner, balance, type and status
Account details
Signed-in home page with the promotion above the account list
Dashboard
Profile page showing name, email, role and member-since date
My details
Deposit form with an account picker and an amount field
Deposit
Withdrawal form with an account picker and an amount field
Withdraw
Transfer form: pick an account, search for a person, enter an amount
Send money
Paginated history table with date, type, amount and resulting balance
Transaction history

What is not done

Stating the boundary is part of the work.


More work on the projects page, or back to Selected Work.