Education / Internal SaaS

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
School management portal dashboard showing fee collection, attendance and admission pipeline modules

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

More on what we build for SaaS & startups

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.

Ask a Quick Question on WhatsApp

Free 30 minutes · no obligation

WhatsAppCall