Story - Gurgant/azurebank-v2 GitHub Wiki
How AzureBank came to be, and three things I do from day one now.
How it started
It started in 2025 as a technical assessment for a .NET role: a small banking application with an API and a web front end.
I wanted to take it beyond the brief. While researching the architecture, I came across the backend-for-frontend (BFF) pattern and decided to use it to keep access tokens on the server, out of the browser. For a banking project, I treated that as a security priority. I did not finish the BFF integration before the deadline. The first version worked, but the BFF was disabled and the web app called the API directly.
In July 2026, I returned to finish what I had started, this time in public: one pull request at a time, key design decisions documented, and tests passing before each merge.
Today, all browser access to the API goes through the BFF, access tokens stay on the server, and the application is live on Azure.
Why a BFF
A single-page app that holds a JWT keeps it where a script can read it. The BFF signs in for the browser, keeps the token on the server and gives the browser a cookie that scripts cannot read: there is no token to steal. The price is one more service and one more network step. See ADR-0001. A script that runs in the page could still send requests while the page is open. It finds no token to take away, and nothing in the session authorises a payment: one PIN entry authorises one payment.
What changed from July 2026
The copy I restarted from had no protection against a payment sent twice, no limit on PIN attempts, no audit trail and no deployment. I brought the code into one public repository and rebuilt it there, one pull request at a time, with tests and written decisions.
- A payment is applied at most once per key.
- One PIN entry authorises one payment.
- The API accepts no caller but the BFF.
- Security events go to a hash-chained table that can be verified.
- The frontend is tested against the contract the API generates, with the real stack running in CI.
- Since 2026-10-06 the app runs on Azure as a live demo, with no database password.
Three things I do from day one now
1. I bind the authorisation to the payment, not to the session. In my first design a PIN entry unlocked the session for five minutes, and every transfer in that time passed. I moved the PIN into each request, and it still proved too little: the same PIN authorised a transfer of 10 and then one of 20. I redesigned it. One PIN entry now authorises one payment: this account, this amount, this payee, for two minutes, once.
2. I ask what a best practice protects before I adopt it. I added refresh-token rotation because it is widely recommended. Then I measured what it did in my architecture: behind a BFF only the server ever holds that token, and in 8 of 8 runs where a renewal met a database that hung, rotation signed someone out. I replaced it with one grant per session and wrote down why, and what I gave up: rotation noticed a copied token in about 15 minutes. Knowing when a common practice does not fit is part of the work.
3. I make every check prove it can fail. A green test is evidence only if it could have gone red. I learned it by auditing my own checks. I built a gate that checks my frontend's mock against the real server: it found differences that every test had passed, and the server was right each time. I also found checks that could not fail: a guard whose pattern matched nothing, a run that passed having tested nothing. I made each of those able to fail. Today the server generates its own contract, the mock is checked against it, and tests run on the real stack on every pull request. A new check counts only after I have broken, on purpose, the thing it guards and watched the check catch it.
Dates you can check
- Every decision record carries its date in its first lines, and the index lists the records by theme: Decisions.
- When a record changed after it was accepted, its first lines name the dates, and the earlier wording is in the git history.
- What I chose not to build, and why: Deferred.
What comes next
The visual design is not final: an interface redesign is planned.
Back to Home.