Client product · rebuilt end to end
Licensure & Tutoring Platform
A health-sciences licensure and school-tutoring platform, rebuilt from scratch as a NestJS modular monolith with React and Next.js — the exam engine walled off in its own schema and role.
17 modules + a kernel · 697 features across 18 milestones · exam schema with its own role
The problem
Two products share one platform here, and they are not alike. A nursing graduate preparing for a licensure exam wants a question bank, papers built from their weakest topics, and marks they can trust. A school student wants a tutor, a timetable and homework. A guardian wants one page that tells them how all three of their children are doing — one of whom is sitting a licence and therefore has no attendance, no homework, and none of the figures the other two have.
Underneath sit the things that make it a real business rather than a content site: tutors with rates and hours across time zones, bookings that must never collide, live lessons, bank-transfer and card payments in currencies with different numbers of decimals, refunds, payouts, and an exam room whose answer key must never reach a candidate early.
The first version was a Laravel modular monolith with Livewire and Filament, and it proved the product. I then rebuilt it from scratch, against a plan that mapped every feature to a milestone before any code was written.
The shape of the rebuild
Three apps, one contract. A NestJS API, a React single-page app for every signed-in audience — learners, tutors, guardians, authors, markers, finance and the operations console — and a Next.js public site. Zod schemas in a shared contract package are the single source: they generate the OpenAPI document, the typed client and the mocks, and CI fails if the generated contract is stale.
Seventeen domain modules plus a kernel, each owning its own PostgreSQL schema: identity, curriculum, tutoring, scheduling, live-room, coursework, library, assessment, practice, progress, billing, entitlements, family, community, messaging, notifications and ops. A module reads another only through its public interface, through a read model fed by its events, or by a plain ID with no foreign key. CI enforces that twice: an import-boundary lint that allows one public entry per module, and a database test that fails if any foreign key crosses a schema.
The exam engine is a fortress. Assessment has its own schema, its own database role and connection, and never joins another module’s transaction. Question versions are append-only, five permissions work independently, and a structural test asserts that the candidate-facing type has no key fields at all — no key, accepted pattern or rubric in any candidate response before reveal or marking, across all nine answer types. There is deliberately no shared “eligibility” port: practice checks entitlement and device, then calls assessment with an actor reference.
Rules the database keeps
- No double-booking, as an exclusion constraint on half-open time ranges. It’s a permanent CI
job, not a one-off spike: back-to-back hours are accepted, an overlap raises
23P01, and unbounded ranges are refused. Every calendar write also takes a per-tutor advisory lock. - Money in minor units with each currency’s own exponent, so the Jordanian dinar formats with three decimals, and a money library that refuses floats and mixed currencies. Ledgers are append-only; credit is debited at order placement under a per-person, per-currency lock; invoices are gapless per seller entity, with the year decided in Asia/Amman.
- Jobs and the transactional outbox live in Postgres, claimed with
SKIP LOCKED. Redis only arrives with a second API node. - External calls are idempotent and distrusted. Card payments go through HyperPay’s hosted form, so card data never touches the platform; the browser’s redirect is never trusted, and the server fetches the payment status itself. Webhooks are signed, replay-protected and processed once.
Arabic and English are equal citizens throughout: logical CSS properties only (a lint fails physical directions), Arabic plural forms, Latin digits, both themes, and a calm twin for every motion.
How it was built
The plan mapped 780 features, of which 697 were in scope, to 25 requirement groups across 18 milestones, and a script verified that every in-scope feature was assigned exactly once. Every milestone shipped API, database, screens and QA together, demonstrable end to end on staging with Arabic and English seed data. Two checkpoints proved the whole product: A, the full money path — a learner books and holds an hour, pays by transfer or card, both join the live room, the tutor signs off and earnings move from held to available; and B, the licensure path — a candidate subscribes to a bank, builds a practice paper, sits it in the exam room and reviews their mistakes.
The working rules were strict on purpose. Only QA wrote tests, from the acceptance criteria, before the code. An architect agreed each milestone’s contract at entry, and a database reviewer signed off whenever money, scheduling or the exam schema changed. From the first identity milestone on, an authorisation-matrix test runs every persona against every route and fails on any route nobody registered.
The last milestone made it ready to run for real: deployment from CI, continuous WAL archiving with a restore drill against a 15-minute recovery point and a 4-hour recovery time, a seeded demo dataset with a one-step reset, a security pass, and a load test that includes an exam room with 200 candidates.
It runs self-hosted in Docker Compose, with local equivalents for every external dependency — a mail catcher, MinIO, ClamAV, a HyperPay emulator and a BigBlueButton stub — so the whole platform can be developed and tested without a single cloud account.
What carries over
The first version’s lessons became acceptance criteria rather than things to remember: the database, not the application, guarantees that two lessons can’t overlap; the guardian’s page says out loud when a lesson is still waiting on its tutor’s sign-off instead of quietly leaving it out; and every milestone shipped with realistic Arabic and English seed data, because the first version found that Arabic questions with English drug names between the gaps are the most reliable way to break a right-to-left layout.
The rebuild added its own in the same spirit: a tutor’s no-show never counts against a child, and a refunded place gets exactly the same “no access” message as one never bought.