← All case files

Case file 008School LMS

The problem

“We're a school, not a spreadsheet company. Everyone uses different tools, nobody knows what's happening.”

Solved

For
Schools: students, teachers, parents, admins
Built
Multi-tenant School LMS — four Next.js frontends, Express backend
Stack
Next.js · React · Express · MongoDB · Docker
Result
MVP feature-complete
Before: one teacher, every paper, every message, every call3D pixel film · made by me

01 · The pain

A school, without a system.

Monday 8am

A teacher checks email. 47 unread. Three from parents asking “is this due today?” Three from students asking for extensions. One from the principal about the fire drill.

No integration. No single source of truth.

Tuesday 3pm

A student submitted an assignment. Somewhere. They emailed it, or pasted it in Google Classroom, or turned it in on some other platform the teacher uses. Is it in, or late, or lost?

Five different apps, nobody coordinates.

Wednesday 5pm

A teacher grades the submission. The grade goes in a spreadsheet on their computer. A parent will see it maybe eventually, on a different platform. The student already moved on.

No automated notifications. No live view.

Thursday 9am

A teacher takes attendance on paper. Later, they type it into a different system. Or they don’t, and the office calls asking who was absent.

Manual, error-prone, invisible to parents.

Friday 4pm

A parent wants to know: is my kid on track? How are their grades? What are they learning this week? They have to ask, or wait for a report card, or guess from conversations.

No transparency. No way to stay informed.

Monday again

An admin tries to add a new class. Is anyone enrolled? Do we have a roster? Who’s teaching what? It’s in three spreadsheets. Or is it in email. Or are we still using the old list from August.

No visibility. No system of record.

02 · The fix

Four roles, four views, one truth.

Switch between student, teacher, and parent. Same school event — a marked assignment, a submitted quiz, an absence. Each role sees exactly what they need.

Biology 101

Logged in as Alex Johnson

Mitochondria Cell Report

Submitted
Due
Oct 15, 2026
Your submission
Oct 14, 3:45pm
Your grade
92 / 100

You can: View your assignments, submit work, see grades and feedback, view class materials.

You cannot: See other students’ work, grade assignments, edit the class, view attendance rolls.

03 · How it works

Three layers, and one hard problem.

  1. Four roles, separate apps

    A student has zero reason to load teacher code. Four independent Next.js deploys (student, school, parent, super-admin), each with its own bundle and backend client.

  2. One backend, tenant-aware

    A single Express + MongoDB server handles all four frontends. Every query auto-injects the current tenant's ID via AsyncLocalStorage — a teacher's query literally cannot see another school's data, it's enforced at the model layer.

  3. Routes, roles, rules

    Middleware chain: requireAuth → requireTenant → requireRole. Assignment status is computed, not stored (assigned/missing are derived at read time). Group submissions, resubmissions, quizzes with feedback gates, attendance with QR anti-replay — all built on the same tenant-safe foundation.

The hard part

Tenant isolation that can’t fail at the UI layer.

A student’s frontend cannot contain the route to grade submissions. But frontends are just code — you can’t enforce a boundary that lives only in JavaScript. The real isolation happens in the backend: every Mongoose query is wrapped by a tenantScoped plugin that reads the current tenant ID from a request-scoped AsyncLocalStorage and auto-injects it into the filter. A teacher’s GET /assignments query becomes{sectionIds, tenantId} server-side. If there’s no tenant in the request context, the query throws instead of running unfiltered.

So isolation isn’t a rule every developer has to remember in every route. One school’s teacher can’t read another school’s data, because a query without the school attached simply won’t run.

Four frontends

Student
School
Parent
Admin

One backend

/assignments/:id

AsyncLocalStorage

tenantId

Mongoose plugin

{ tenantId }

MongoDB query

find(filter)

04 · What changed

School management became a system, not a spreadsheet.

Schools onboarded
—

Metric to confirm · active tenants

Concurrent users
—

Metric to confirm · peak load

Assignment submission rate
—

Metric to confirm · students turning in work