An entire school operation, from enquiry to fee receipt, in one portal
Admissions, enrolment, attendance, fee collection, parent communication, finance and reporting — with four distinct user experiences on a single codebase.
- Client
- Little Lumos Schools
- Industry
- SaaS & Startups

What was the problem?
A growing preschool group was running academics, fees, attendance, admissions and parent communication across separate registers, spreadsheets and messaging groups. Nothing reconciled against anything else, so every parent question became a search and every fee cycle became manual arithmetic. Adding a second campus would have multiplied the problem rather than the workload.
What made it hard
Commercially
- Fee changes must never retroactively alter historical obligations
- A school configures its own roles, but must not be able to misconfigure its way into a data breach
Technically
- Every query has to resolve to the correct academic-year window, with period keys, month ranges and timezone-correct school dates
- Promoting a whole cohort forward must be atomic, with rollback on any failure
- A retried 'Generate fees' run must not double-bill hundreds of students
- Three distinct user experiences share one codebase without leaking each other's data
Operationally
- An administrator messaging 400 parents cannot be left waiting on SMTP
- Bulk student import must fail cleanly and completely rather than half-importing a spreadsheet
At scale
- Admission IDs must stay unique under concurrent bulk imports
- 500-row import cap with atomic rejection
Security
- Teachers must be structurally unable to reach another class's data through the API
- Parent contact details must be masked server-side, not merely hidden in the UI
- Tenant scope must be enforced on every request
What we built
A full-stack school management portal built from the ground up: multi-tenant architecture, role-based access for superadmin, admin, teacher and parent, automated fee generation and collection, attendance, daily activities, the admission pipeline, expenses, inventory, events and notifications.
What it does
Admissions funnel
Enquiry capture with source attribution through to enrolled student record.
Versioned fee structures
Immutable fee versions bound to students at enrolment, with add-ons and concessions.
Fee generation and collection
Monthly and annual generation, line-item payments, part-payments and PDF receipts.
Attendance and daily activity logs
Attendance marking plus meals, naps, mood and learning logs.
Unified student timeline
Attendance, activities, fees and events merged into one feed with per-viewer redaction.
Role-based experiences
Superadmin, admin, teacher and parent views plus custom roles, driven by server-issued permissions.
Year-end promotion
Dry-run preview then transactional cohort commit with rollback.
Notification composer
Role, branch and student targeting with branded HTML email delivery.
Analytics dashboards
Enrolment, attendance, fees, expenses and enquiry dashboards with CSV and PDF export.
Bulk Excel import
28-column template, 500-row cap, atomic all-or-nothing commit.
Receipt and document sharing
Delivered over both email and SMS.
Inventory and expenses
Operational record-keeping alongside the academic data.
PWA parent portal
Installable, with a conservative service worker that never caches authenticated data.
Engineering decisions
Choices that could have gone another way, and what each one bought.
Fee structures are versions, not rows
A fee structure is an immutable version. A student is bound to the version active at their enrolment, and generation resolves class version → optional add-ons → student concessions → line items → obligation records inside a single transaction with idempotent upserts.
Why it matters. Changing next year's fees can never silently rewrite last year's invoices, and a retried 'Generate fees' run cannot double-bill 300 students.
Four gates, and a deny list no admin can override
Every request passes JWT auth, a role gate, a permission gate and a tenant scope gate. Above that sits a school-configurable teacher allowance system; beneath it, a hard-deny list. Teacher responses are field-masked server-side and class access is verified per record.
Why it matters. A school can tune its own roles without being able to misconfigure its way into exposing fees or analytics to teachers — and a teacher physically cannot read another class's data through the API.
Academic-year scoping as a first-class concept
A dedicated scope resolver translates every query into the correct academic-year window: period keys, month ranges, timezone-correct school dates, and SQL fragments deciding which fees belong to which year.
Why it matters. This is the layer most school systems get wrong, and getting it wrong produces balances and reports that are quietly incorrect across year boundaries.
Year-end promotion is preview-then-commit
A dry run shows exactly which students promote, graduate or are held back. The commit then rolls the entire cohort forward, archives enrolments and re-binds fee versions transactionally, with rollback on any failure.
Why it matters. The riskiest annual operation in a school system becomes something you can check before you do, and something that cannot end up half-done.
Asynchronous dispatch without pretending to be a queue
Publishing a notification returns 201 immediately; recipient resolution and fan-out happen off the request path, with per-recipient failure isolation so one bad mailbox cannot abort a broadcast. Heavy jobs use transactional batches with ON CONFLICT DO NOTHING rather than a job queue.
Why it matters. An admin messaging 400 parents never waits on SMTP. This is deliberately described as a non-blocking dispatch layer rather than a queue, because that is what it is — the natural upgrade to QStash or BullMQ would buy retries, dead-letter handling and scheduled sends.
Technical detail
Collapsed by default. Open whichever part you are evaluating.
- Frontend
- Next.js 15 (App Router)
- TypeScript
- 18 routed pages
- 78 components
- Typed API client (~2.1k lines)
- PWA layer
- Backend
- Express on Node 20
- TypeScript
- 38 domain services (~11.8k lines)
- 154 REST endpoints across 26 route modules
- Database
- PostgreSQL (Neon)
- 30 tables
- 38 versioned migrations
- Custom migration runner with a schema_migrations ledger
- Storage
- Firebase Storage with server-side Sharp compression to WebP, 1.5 MB cap, resize-on-upload
- Authentication
- JWT with bcrypt
- Forced password change on first login
- Token-based password reset
- Authorization
- Four-layer gating: JWT auth, role gate, permission gate, tenant scope gate
- School-configurable teacher allowance system
- Hard-deny list
- Server-side field masking
- Background processing
- Non-blocking fire-and-forget dispatch layer with per-recipient failure isolation
- Transactional batch operations using BEGIN/COMMIT/ROLLBACK with ON CONFLICT DO NOTHING idempotency
- Messaging
- Nodemailer/SMTP branded transactional email
- MSG91 SMS
- Deployment
- Two independent Vercel projects — Next.js frontend and Express serverless API, CORS-locked to allowlisted origins
- Monitoring
- Structured logging with Pino across every dispatch
- Frontend
- Next.js 15
- React
- TypeScript
- PWA
- Backend
- Node.js 20
- Express
- TypeScript
- Zod
- Database
- PostgreSQL (Neon)
- Cloud
- Vercel
- Firebase Storage
- Integrations
- Nodemailer / SMTP
- MSG91
- PDFKit
- ExcelJS
- Sharp
- Tools
- Pino
- Helmet
- bcrypt
- Authentication
- JWT with bcrypt password hashing
- Forced password change on first login
- Token-based password reset
- Authorization
- Four-layer gating on every request: JWT auth → role gate → permission gate → tenant scope gate
- School-configurable teacher allowance system covering parent contacts, documents, downloads and staff directory
- Hard-deny list — teachers can never be granted fees, analytics, admissions or settings access, even by an admin misconfiguring a custom role
- Class access verified per record
- Data protection
- Server-side field masking on teacher responses
- Per-viewer redaction on the unified student timeline
- Service worker caches only immutable static assets, never API responses or authenticated data
- Soft deletes and audit columns throughout
- Abuse prevention
- Helmet security headers
- CORS allowlist
- Global rate limiting plus a stricter 10-requests-per-15-minutes limiter on auth endpoints
- Zod schema validation on every mutating endpoint across 20 validator modules
- Auditability
- Audit columns throughout
- Unified student timeline as an audit-style feed
- Delivery-state tracking in the database so re-sends are explicit and auditable
- Privacy
- Teachers cannot see parent contact details unless the school explicitly allows it
- Database connectivity errors surface as 503 rather than stack traces
Mechanisms
- Transactional batch operations using BEGIN/COMMIT/ROLLBACK
- Idempotent upserts with ON CONFLICT DO NOTHING
- Pre-allocated admission ID queue for concurrency safety
- Per-recipient failure isolation on broadcasts
- Delivery-state tracking in the database
- Preview-then-commit for destructive cohort operations
- Custom migration runner with a schema_migrations ledger
Failure scenarios handled
- A 'Generate fees' run is retried after an uncertain outcome
- One invalid mailbox in a 400-parent broadcast
- Two concurrent bulk imports competing for admission IDs
- A year-end promotion failing partway through a cohort
- A spreadsheet with one malformed row among hundreds
- Database connectivity loss during a request
Idempotency. Batch operations use idempotent upserts inside transactions, so a retried fee generation cannot create duplicate obligations or double-bill students.
Recovery. Destructive or expensive operations are made reviewable before they run (dry-run preview) and atomic when they run (transactional commit with rollback). Communications track delivery state in the database so re-sends are a deliberate, auditable act rather than a guess.
Before and after
Before
- Operations spread across registers, spreadsheets and messaging groups
- Enquiries tracked informally with no source attribution
- Fee changes risking retroactive effects on historical invoices
- No single view of a child's attendance, activities, fees and events
- Parent communication sent outside any system of record
After
- One portal covering enquiry through to fee receipt
- Immutable fee versions bound to students at enrolment
- A unified per-student timeline with per-viewer redaction
- Four role-appropriate experiences from one codebase
- Notifications and receipts sent and tracked from the same system
The build, by the numbers
What was built, at what scale. These describe the system's size, not its business results.
- REST endpoints
- 154REST endpointsAcross 26 route modules
- database tables
- 30database tablesWith 38 versioned migrations and a schema_migrations ledger
- automated tests
- 764automated tests705 backend and 59 frontend
- domain services
- 38domain servicesRoughly 11.8k lines of backend service layer
- role-based experiences
- 4role-based experiencesSuperadmin, admin, teacher and parent, plus custom roles, on one codebase
- validator modules
- 20validator modulesZod schema validation on every mutating endpoint
What changed as a result?
End-to-end school operations automated in one platform, with a single source of truth for academics, finance, staff and parent communication.
- Multi-tenant, so additional campuses are configuration rather than a rebuild
- Four distinct permission levels, so parents and staff see only what applies to them
- Fee generation and collection automated across terms
Why does this matter in SaaS & startups?
Most first builds are too large. The useful MVP is the smallest thing that tests whichever assumption would sink the business if it were wrong. Both of our internal SaaS builds — a multi-tenant school portal and a restaurant POS — started as the narrow system one operator needed, then earned their scope.
Scope that assumes the idea is already validated
Settings screens, admin tooling and dashboards feel like progress and prove nothing. Building them before anyone has paid is the most common way a runway disappears.
Multi-tenancy retrofitted too late
Serving a second customer from a system built for one is an expensive rewrite. Where multiple organisations are clearly coming, tenancy belongs in the first architecture.
No route to a real user
A product that exists only on a laptop cannot be tested. Deployment, accounts and access need to work from the first week, not the last.
Handover that leaves you dependent
Code you cannot access, hosting in someone else's account and no documentation turn your own product into a negotiation. Ownership from day one avoids that entirely.
See it running
portal.littlelumosschools.com(opens in a new tab)Need something like this for SaaS & startups?
Tell us what is slowing the business down and we will tell you whether software is the right fix. If it is not, we will say so.
Free 30 minutes · no obligation
Related work
- AI Workflow Platform
Little Lumos — AI Monthly Progress Reports
A preschool was hand-writing 70+ monthly progress reports, costing 3–4 working days every month. Softivum built the full workflow: teachers speak, AI drafts, admins approve, and branded letters reach both parents with per-recipient delivery tracking.
- Marketplace Platform
Helpora — Hyperlocal Neighbour-Help Marketplace
A marketplace where neighbours pay neighbours for everyday help. Softivum built the consumer app, the marketplace engine, the operations console and the brand site — verifying identity against government records, holding every payment in escrow until the job is done, and tracking helpers live on a map.
- Food & Beverage / Internal SaaS
Pizza Theory POS
Custom POS and operations system: order to invoice in under 10 seconds, with a payment-gated kitchen queue and live order tracking