A school fee and results management platform for Ugandan schools, built and shipped as a real commercial product, not a prototype. Here's what it does, how it's built, and one production bug that turned into a schema-wide fix.
Most schools in Uganda run fee collection and academic results on paper, spreadsheets, and phone calls, three systems that never agree with each other. A parent calls to ask if they've paid; the accountant checks a ledger book. A teacher wants a class average; someone reconstructs it from graded scripts. At the end of term, someone manually merges a fee balance and a report card into one document a parent can actually use.
I built a multi-tenant platform that replaces all three with one system, designed from the start to be sold to multiple schools as a product, not built for a single school's internal use.
Every feature below is live and in use, not roadmap.
The kind of problem that only surfaces once something real depends on the system.
Late in the build, I added a "delete school" action for platform admins, a real, permanent, cascading delete of a tenant and every record tied to it. The first attempt to run it in production failed with a generic error.
Tracing it down turned up something bigger than the feature itself: 27 relationships across the schema, between exams and classes, fee structures and terms, attendance and students, had no defined behavior for what should happen to them when something they depended on was deleted. Every model's own link back to its school had been carefully set up to cascade correctly; the connections between business objects underneath that had not.
That gap wasn't only a deletion problem; it would have silently blocked deleting a single class, term, or subject anywhere else in the app, the first time anyone tried. Fixing it meant deciding, relationship by relationship, whether a dependent record should be deleted with its parent or just detached, including a deliberate call to make audit-log entries survive a tenant's deletion, unlinked rather than destroyed, so accountability outlives the thing it's about.
The fix shipped as a database migration, verified against the live production database before being trusted.
ExamSubject.subject → Subject had no onDelete rule, one of 27 found the same way.This isn't a demo. The first pilot school onboarded onto the platform with its real class structure, real fee amounts, and real enrolled students, not seed data, and has been running live fee collection and results tracking on it since.
The platform is live in production, handling real fee collection and results for an onboarded school today.
View the live product Get in touch