Osubula Talks Solutions
Salt
Builder, Osubula Talks Solutions
View the live product
Case study, multi-tenant SaaS platform

One system replaced a register, a spreadsheet, and the phone calls.

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.

5
Roles with distinct, enforced access
27
Schema relations found and fixed in one audit
1st
Pilot school running real fee & results data

The problem

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.

What it does

Every feature below is live and in use, not roadmap.

Fee billing that bills itself
A fee structure is set once per class and term. Every student in that class gets a correct, itemized balance immediately, no manual billing per child.
Payments, three ways
Cash or bank, recorded by an accountant in seconds. Or mobile money, MTN, Airtel, paid directly by a parent from their phone via Flutterwave, settling straight into the school's own account. The platform never holds a school's fee money; that's an architectural constraint, not a policy.
Grading that matches the real curriculum
Compulsory subjects, weighted Continuous Assessment / End-of-Cycle scoring, Uganda's actual UNEB rules, not a generic percentage scale bent to fit. A composite-results feature combines a mock, a midterm, and a final into one certificate-style result with its own weighting.
Built for unreliable internet
Recording a payment, entering scores, adding a student; all of it keeps working offline. Everything queues on the device and syncs the moment signal returns, with nothing lost and nothing recorded twice.
Everything else a school runs on
Daily attendance, printable photo ID cards, bulk CSV import for rosters and exam scores, SMS fee reminders sent in bulk with each family's own balance, and combined fee-and-academics report cards as PDF.
Isolated by design
Five roles, Super Admin, School Admin, Accountant, Teacher, Parent, each enforced at the database query level, not just hidden in the UI. A teacher's own class, a parent's own child, and nothing else.
Built with
Next.js 16 TypeScript Tailwind CSS v4 Prisma PostgreSQL Supabase Auth.js react-pdf Vercel

A production bug that exposed a schema-wide gap

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.

Before Exam ExamSubject Subject ? After Exam ExamSubject Subject
ExamSubject.subject → Subject had no onDelete rule, one of 27 found the same way.

Real-world validation

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.

See it running.

The platform is live in production, handling real fee collection and results for an onboarded school today.

View the live product Get in touch