Meridian
A university platform with three role-specific portals on shared infrastructure, built to test whether institutional complexity is a question of boundaries rather than of styling.
The problem
- Role portals
- 3
- Screens across them
- 29
- Authenticated shells
- 1
Institutional software is hard because it serves radically different users at the same time.
A registrar configuring a semester, an instructor grading a submission queue at midnight, and a student trying to understand why a grade moved have almost nothing in common. Different information hierarchies, different workflows, different relationships to urgency.
Building one of those portals is a CRUD exercise. Building all three on shared infrastructure turns the interesting question from styling into boundaries: what the shell owns, what each portal owns, and where a shared component stops being a saving and becomes a liability.
Architecture
Four layers, each with one rule. Server components fetch and shape; client components own interactivity and never fetch. The data universe is seeded and relational, so every figure on a dashboard is derivable from the records it claims to summarise. Three portals share one authenticated shell and diverge entirely below it.
Server components fetch and shape; client components own interactivity and never fetch. A server-only import in the data layer makes that a build error rather than a convention.
Seeded and relational rather than fixtures. Courses hold students, deadlines belong to courses, and every dashboard aggregate derives from the records it claims to summarise.
Three portals on one authenticated shell. The shell takes role-agnostic props; each portal owns its navigation, its data shape and its empty states.
Plain CSS with custom properties, scoped to a data-theme attribute. The JSX names decisions — a card, an active nav item — rather than assembling each one from atoms.
// src/fake-db/universe.tsimport "server-only";import { buildCourses } from "./courses";import { buildStudents } from "./students";import type { Universe } from "./types";function build(): Universe { const semesters = buildSemesters();The interface
The admin overview is the densest screen in the system and the one worth arguing about: every summary an institution needs, in one view, without the reader having to work for it.
Enrolment, throughput, grading load and activity are aggregated server-side and arrive with the page rather than as four client requests.
Density without noise. The screen answers what moved since yesterday first, and everything else is subordinate to that.
Actions sit beside the figures they affect, so reading and acting are one gesture rather than two screens.
Performance
Measured after the build rather than assumed during it, on the screen that aggregates the most: caching the derived aggregates and memoising the shaping took the overview's data from 0.75s to 0.36s. Against the live deployment Lighthouse scores 94 on mobile and 100 on desktop, with nothing blocking the main thread and nothing moving once it paints. The exception is the first request of the day, which takes 6.5s — the deployment starting rather than the application rendering.
Across three role portals on one authenticated shell
Seeded relationally, so every aggregate derives from the same records
From 0.75s, by caching the derived aggregates
- The no-utility-class rule holds everywhere but twenty screens, where it broke as a one-off margin.
- server-only guards one file of twenty-nine. The barrel is safe; a direct import of a generator is not.
- Every form has a sending state. It is a 1.4 second timer, and what it saves is gone on navigation.
- The README promises deterministic seeding. The code seeds on Date.now().