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
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
Feedback from Ms. Rivera:
Excellent diagrams and explanation. Good use of peer references.
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.
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.
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.
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
One backend
/assignments/:idAsyncLocalStorage
tenantIdMongoose plugin
{ tenantId }MongoDB query
find(filter)04 · What changed
School management became a system, not a spreadsheet.
- Schools onboarded
- —
- Concurrent users
- —
- Assignment submission rate
- —
Metric to confirm · active tenants
Metric to confirm · peak load
Metric to confirm · students turning in work
