Skip to content
IndexCase 01
Learning platform

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.

Role
Self-directed
Timeline
3 weeks
Stack
TS · Next.js · Radix
Team
Solo
meridian-university.vercel.appmain · seeded
Overview
Admin overview
Students
14,820
Courses
1,184
Instructors
612
Screens
29
Fig. 01 — Meridian, admin overview2026
Chapter 01

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.

Chapter 02

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.

Layer 01
Boundary

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.

Layer 02
Data universe

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.

Layer 03
Role shell

Three portals on one authenticated shell. The shell takes role-agnostic props; each portal owns its navigation, its data shape and its empty states.

Layer 04
Design system

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();
Chapter 03

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.

Admin overview
Interaction notes
One place, one payload

Enrolment, throughput, grading load and activity are aggregated server-side and arrive with the page rather than as four client requests.

Readable before impressive

Density without noise. The screen answers what moved since yesterday first, and everything else is subordinate to that.

Controls where the data is

Actions sit beside the figures they affect, so reading and acting are one gesture rather than two screens.

Fig. 02 — Admin overview2026
Chapter 04

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.

Overview data — before0.75s
Overview data — after0.36s
First paint — cold start6.5s
First paint — warm0.7s
Outcome
29
Screens

Across three role portals on one authenticated shell

14,820
Students

Seeded relationally, so every aggregate derives from the same records

0.36s
Overview data

From 0.75s, by caching the derived aggregates

Next case — 02
Cairn
  • 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().