Case study
Institutional Finance & Research Platform
A multi-tenant ERP that runs budgeting, expenditure, settlement and research operations for a 39-department engineering institution. Live for 14+ months, 330+ active users, and ₹43.4 Cr of budget processed through workflows I designed and built.
React/TypeScript/Node.js/Express/MySQL/CASL/JWT/Google OAuth/Azure MSAL/Vitest
₹43.4 Cr
Budget processed
across 1,000+ proposals
330+
Active users
across 39 departments
570+
Finance requests
advances, claims, settlements
14 mo
In production
continuously, no rewrite
01 Context
Paper forms moving crores.
A 39-department institution ran its budgeting, expenditure and research funding on paper forms, spreadsheets and email. A single proposal could touch four desks before anyone committed money, and none of those handoffs left a record anyone could query later.
The consequences were mundane and expensive. Nobody could answer “how much of this head is already committed?” without phoning around. Approvals stalled with no way to see where. At audit time, reconstructing the trail for a single settlement meant digging through folders.
The brief was not “digitise the forms”. It was to make the money traceable — every rupee attributable to a head, a proposal, an approver and a date.
02 Approval engine
One engine, many workflows.
Budget proposals, expenditure claims, advances, settlements and journal submissions all look like different forms to a user. Structurally they are the same thing: a document that moves through ordered stages, where each stage can approve, return or reject, and where returning must not lose the history.
Building five separate workflows would have meant five places to fix every bug. I built one engine and configured the stages per document type.
- Originator — Raises a proposal against a budget head
- Department head — Verifies scope and available balance
- Finance — Checks heads, ceilings and prior commitments
- Sanctioning authority — Approves, returns or rejects with reason
Two decisions did most of the work. First, a returned document goes back to the originator as the same document with an added event, not a fresh copy — so a proposal rejected twice still reads as one continuous history. Second, the balance check runs against committed funds rather than spent funds, so two proposals racing against the same head cannot both pass.
03 Access control
Roles the institution owns.
The obvious implementation is an enum of roles in the codebase. It is also the one that guarantees a deploy every time the institution reshuffles a committee — and they reshuffle often.
So roles are rows, not constants. Each carries a code, a display name, and flags marking whether it is system-owned or administrator-assignable. An administrator composes roles and scopes them to an organisational unit — a department, centre, cell or professional body — without an engineer being involved.
Permissions are expressed with CASL and shared between server and client, so the same rules that authorise a request also decide whether a button renders. The UI never offers an action the API would refuse.
Authentication runs through two identity providers — Google and Microsoft — because staff and students were already split across both. Sessions are JWT; authorisation is always re-derived server-side.
04 Data model
Money needs an audit trail.
Finance software is judged on whether it can explain itself a year later. The schema is built around that: allocations, commitments and disbursements are separate concerns, and state changes are recorded as events rather than overwritten columns.
Every schema change ships as a forward migration — 33 of them so far — because a live system holding real financial records cannot be reset. Supporting documents (bank statements, annexures, claim evidence) are stored per category with validation on type and size, and referenced rather than embedded.
The parts most likely to cause a quiet financial error — annual rollups, claim ownership, workflow transitions, notification de-duplication — are covered by 35 test suites. That is where the tests are, because that is where a bug costs someone real money.
288
REST endpoints
across 23 route modules
118
MySQL tables
33 forward migrations
18
Domain modules
budget, finance, journal, events…
35
Test suites
workflow, claims, auth, rollups
05 Outcome
What it does now.
The platform has run continuously for 14+ months without a rewrite. It serves 330+ active users across 39 departments and has processed ₹43.4 Cr across 1,000+ budget proposals, plus 570+ finance requests covering advances, reconciliation and settlements.
The outcome I care about most is not a number. Questions that used to require a phone call — where is my proposal, what is left in this head, who approved this and when — are now answered by a page.
06 Retrospective
What I would change.
I would put the workflow engine behind a state machine definition sooner. The stages were configurable from early on, but the transition rules lived in code longer than they should have. Every new document type meant re-reading that code to be sure it was safe.
I would add the audit trail on day one, not after the first audit question. Retrofitting event history onto tables that already had rows is meaningfully harder than designing for it, and some early records are thinner than I would like.
I would introduce typed API contracts earlier. The client is TypeScript and the server is not, so the boundary was hand-maintained for too long. Generating clients from the OpenAPI specs — which exist for ten domains — would have removed a whole category of integration bug.
Want the detail?
The source is closed, but I am happy to walk through the architecture, the schema decisions or the approval engine in conversation.