Consolidating a training centre that ran on four separate programs
- Client
- Name withheld
- Sector
- Driver training and vocational certification
- Year
- 2026
Problem
Driving school, professional competence, psychotechnical assessment and dangerous-goods training were tracked separately. The same candidate was registered again in every module, and payment figures differed depending on which panel you opened.
Approach
I anchored the money to a single source of truth first, then rebuilt the modules on top of it. Enrolment as an event was separated from the financial transaction.
Architecture
- Multi-tenant structure — each organisation isolated, modules licensed separately
- One financial truth: every charge and payment in a single transaction table
- Enrolment separated from the transaction, so one candidate can join several modules
- Reconciliation endpoint that drives the gap between panel balances and transactions to zero
- Government portal transfer: field-mapped autofill, with a human still pressing save
- Android application (Capacitor) for staff working on site
Result
| Metric | Before | After |
|---|---|---|
| Candidate record | Separate record per module | One candidate, many modules |
| Payment figures | Differed by panel | Single source plus reconciliation report |
| Portal data entry | A full working hour, every field by hand | Autofilled, human approves |
| System status | Four separate programs | In production, in daily use |
Stack
- TypeScript
- React
- Express
- PostgreSQL
- Drizzle ORM
- Docker
- Capacitor
The relationship, stated up front
This was not an arm’s-length client engagement: the request came from a company in which my business partner holds a stake. I am not hiding that, because a case study exists so the reader can compare it to their own situation — not only to show that I can do the work.
Neither the work itself nor the fact that the system runs in production today depends on that distinction.
It started as a request for an ERP
The ask was an ERP. But the problem I diagnosed was not resource planning — it was that the same person existed four times, with four different records. A candidate taking both a certification course and a psychotechnical assessment was registered twice, tracked in two places, and two panels could show two different balances.
An off-the-shelf ERP would not have solved that. It would have imposed its own record model and reproduced the same disconnect in a different shape.
The decisive call: money first, modules second
The turning point was a separation: enrolment is one event, payment is another.
They had been entangled, and that entanglement was the source of the inconsistency. I moved every financial movement into a single transaction table and kept the enrolment record separate. A candidate can now join several modules while their money stays visible in one place.
I added a reconciliation endpoint on top: it computes the gap between the balances shown in the panels and the sum of transactions. If that gap is not zero, something is wrong — and I see it before the user does.
Where I chose not to automate
Entering candidate data into the government portal was the most time-consuming task. I tried full automation; the portal’s captcha made it impractical.
Instead I built field-mapped autofill: the data is prepared and placed into the form, but a human still presses save. Leaving the step that writes into an official system under human control was a deliberate choice, not a limitation.
Security: two critical findings while live
After launch I ran a full security review. Two critical findings came out, both live at the time:
- File upload and download endpoints required no authentication and allowed path traversal — candidate documents were unprotected.
- The single function all authorisation passed through did not check roles, so a student account could reach the organisation’s back office.
Both were fixed. I include this because a system with no findings is a system nobody audited. My own code had critical holes; I found them and closed them.
Today
The system is in production and in daily use. We fix things as users report them, and most recent additions came out of that feedback — the mobile interface, for example, was redesigned after a user said the panel “looks like a website and booking an appointment is hard”.