Multi-tenant school ERP
Built and running on a staging environment, in active development rather than general release. Figures above were counted from the codebase.
Multi-tenancy is where school platforms usually leak. One school seeing another school's students is not a bug you recover from commercially, so isolation could not depend on remembering a WHERE clause.
The working parts
Each of these exists in the delivered system, not on a roadmap.
Tenant isolation in the database
PostgreSQL row-level security is enabled and forced on every tenant table, and the application connects as a role that cannot bypass it. A missed filter in application code cannot leak another school's data.
Operator console
Onboarding request review, tenant provisioning, subscription plans, module entitlements, platform invoicing, operator team management and an audit trail.
School administration
Campuses and branches, academic years, terms, classes, sections, subjects and per-school branding, with a campus scope selector that switches the whole navigation rail.
Admissions pipeline
Configurable stages with stage history and events, guardian records, offers, and a public application form per branch that needs no login.
Student and staff records
Enrollment, guardians, employee records and Elasticsearch-backed search, with CSV and Excel export.
Attendance, timetable and examinations
Separate student and employee attendance with holidays, timetable periods and slots, class and teacher subject mapping, exams, grading scales and results.
Fees and vouchers
Fee heads, fee structures, voucher templates and generation, payments, and a payment-proof review flow. Money is stored as integer minor units with an explicit currency, never as a float.
Dynamic form builder
Admission, student and teacher forms are configurable records with typed fields and stored submissions, so a school changes a form without a release.
Permissions as data
64 wildcard-capable permission codes resolved per user at login, so changing what a role can do is a data change rather than a redeploy.
Background jobs and exports
A separate worker process on BullMQ handles queued work, with Excel and PDF generation off the request path.
Where people actually work.
- 01Operator overview and school directory
- 02Onboarding request review
- 03Subscription plans and platform invoices
- 04School admin dashboard with campus scope
- 05Academic structure and campus subjects
- 06Admissions pipeline and application detail
- 07Students, teachers and attendance
- 08Fee setup and voucher generation
- 09Public admission form per branch
- 10Parent portal: applications and vouchers
- SMTP email with templated notifications
- Elasticsearch for student and staff search
- Redis and BullMQ for queued work
- Cloudflare DNS with a wildcard TLS certificate
- GitHub Actions deploying to a Docker Compose host
The calls that matter later.
The choices that separate software people tolerate from software they rely on.
Row-level security instead of a trusted query layer
Tenant context is set as a transaction-local Postgres setting and policies are forced, so isolation holds even if a developer writes a raw query. It costs a little migration discipline and removes an entire class of catastrophic bug.
One source tree, two builds
The operator console and the school portal ship from the same repository with a build-time surface flag, so the other surface's code is tree-shaken out. Shared components stay shared without shipping the operator's endpoints to a school.
Money as integer minor units
Every amount is a bigint of the smallest currency unit plus an explicit three-letter currency. Floating-point money in a fees system produces disputes that cost more than the feature.
More work
View allWant one built around your operation?
Discovery takes one to two weeks and produces a process map, wireframes and a fixed quote. Useful documents even if you take them somewhere else.
We reply to every enquiry within one business day.