Amica School Portal
amicaschoolportal.comAmica replaces the spreadsheets, paper registers, and WhatsApp messages Nigerian private schools run on with one platform for fees, results, attendance, and parent communication, built to serve many schools on shared infrastructure without any one school's data ever being reachable from another's.
The Outcome
Multi-tenant isolation is enforced at the database layer, independent of application code, and verified with real cross-tenant queries against a real Postgres test database rather than assumed to work. Separately, a dedicated billing-reliability pass closed a payment path that could let Paystack capture a real charge and never record it in Amica, caught with a test-mode payment during hardening, before any live school depended on it.
Cross-school data leakage is blocked at the database layer, tested by attempting the exact query that would cause it and asserting it fails.
A school in any billable state (pending, expired, or in its grace period) can always reach its own billing page to pay.
Every completed payment is guaranteed to be recorded, verified against a real Paystack test transaction before general use.
The Context
- Manual processes don't scale, and they fail quietly. Fee tracking in spreadsheets, report cards typed and printed per term, payment confirmations sent by personal WhatsApp: the failure modes (lost records, double charges, missed notifications) are real, and nothing affordable was built for the actual operating context, Nigerian payment gateways, Naira as the unit, SMS delivery that has to work on feature phones.
- A multi-school platform is a trust problem before it's a features problem. Once fee and result records for several schools live in the same system, a school considering the platform has to be convinced its students' data cannot leak to another school on the same infrastructure, before anything else about the product matters.
- Billing has to work for a school that hasn't paid yet, not just one that has. A school in a pending, expired, or grace-period state still needs to reach its own billing page to fix that, or it has no way back to active use.
The Decision
The questions, trade-offs, and architectural reasoning behind how this product was built.
Closing the one path where a real payment could vanish
Subscription billing is gated by account status: schools in a pending, expired, or grace-period state are blocked from most of the app.
- Loosen the cookie policy site-wide to keep the callback authenticated
- Remove authentication from the callback route specifically and authorize it a different way
Loosening cookies site-wide would widen the blast radius of a fix meant for one route. Removing auth from just the callback was safer, provided the route's new authorization boundary (a Paystack-verified transaction plus server-set metadata Paystack only ever echoes back) was actually as strong as a login.
Removed authentication from the billing callback route, and treated everything that removal put at risk as its own problem to close in the same change: every failure branch was collapsed into one identical response so a caller can't fingerprint valid references by the error text, the incidental rate-limiting a login step provided was replaced with an explicit IP-based limit that fails open (not closed) so a Redis outage can't itself cause a real payment to be dropped, and every request now writes an audit log line before anything else runs.
The subscription now activates correctly regardless of session state, verified against a real Paystack test-mode payment, not a mocked test, before shipping.
Found and closed during a dedicated hardening pass, before a live school ever relied on it.
Choosing database-enforced isolation over trusting application code
Three approaches were viable for keeping one school's data invisible to another: separate databases per school, application-layer scoping only, or PostgreSQL Row-Level Security.
- Separate databases per school
- Application-layer scoping only
- PostgreSQL Row-Level Security with a single shared database
Defense-in-depth is the only acceptable strategy when handling another institution's financial and student records. Row-Level Security gives that without the operational cost of a database per school.
Application-layer scoping stays as the first line of defense, but Row-Level Security at the database layer is the one that actually has to fail for a leak to happen.
Testing this honestly required switching the test suite from SQLite to a real Postgres database, since SQLite doesn't support the session-parameter mechanism Row-Level Security relies on, closing a category of false-positive test passes in the process.
The database is the last line of defense, not the application code.
The Execution
- PostgreSQL Row-Level Security as a second, independent isolation layer. Every tenant-scoped query is filtered by school at the application layer, but that alone only holds if every developer remembers the filter every time. A Postgres session parameter, set once per request before any query runs, means the database itself refuses to return another school's rows even if the application-layer filter is ever missed.
- Kobo-only currency storage. Every monetary amount is stored as an integer in kobo, never a float or a decimal column, so rounding errors in fee calculations are eliminated by construction rather than caught in testing.
- Horizon-managed background work from day one. PDF report card generation, SMS delivery, and bulk imports run as queued jobs behind Laravel Horizon, configured before a single job existed, so the pattern of "the queue can wait" never had a chance to take hold.
Client Feedback
What clients say after shipping.
“I’m proud of what ThinkDare has built. He listened closely to our needs and turned our vision into a portal that makes school administration smoother and keeps our community better connected. His skill, care, and commitment have made a real difference to Amica. We’re grateful for The work and excited about what we can accomplish together next.”