Platform Console
The operator console for onboarding schools, editing shared curriculum versions, and managing platform operators.
Every other surface in the app belongs to a single school. The Platform Console is the one place that sits above all of them — the operator's control plane for the whole multi-tenant platform. It's where a school first comes into existence, where the shared curriculum templates that schools build on are authored, and where the handful of people who run the platform itself are managed.
It lives under /platform, and the door is guarded: the route's beforeLoad calls requirePermission({ userType: 'platform_admin' }), so only platform admins ever get in. Everyone else is bounced before the page renders.
export const Route = createFileRoute('/_authenticated/platform')({
beforeLoad: requirePermission({ userType: 'platform_admin' }),
component: RouteComponent,
})A different shell
The console has its own sidebar and navbar — PlatformSidebar and PlatformNavbar, not the staff-hub ones. If you find yourself in a layout that says Platform in the corner, you're in the operator world, looking at platform-level data rather than one school's. See Routing and App Shell for how shells are picked.
Three areas
The sidebar (platform.sidebar.tsx) lays out everything the console does, plus a Hub landing page with platform-wide stats:
- Schools — onboard new tenants and configure existing ones.
- Curriculum — author the versioned academic templates schools build on.
- Operators — manage the platform-admin accounts themselves.
Each is mostly a list screen that opens into a detail screen — a familiar shape you'll meet again and again. Let's walk them in turn.
Schools
A school is a tenant: its own users, its own data, its own branding, all isolated from every other school. The Schools list at /platform/schools is the registry of every tenant on the platform, rendered as a data table with a status badge and a row menu to suspend or re-activate each one.
The interesting action is Provision school. It opens a dialog that does two things in one call — creates the empty tenant and its first super-admin staff user — then tells you the new admin can sign in via OTP to take it from there:
provisionSchool(value, {
onSuccess: () => {
toastManager.add({
title: `School "${value.name}" provisioned`,
description: `${value.adminEmail} can now sign in via OTP to set it up.`,
type: 'success',
})
},
})Suspending a school is deliberately heavy — it denies access to every user in that tenant — so the confirm surface makes you type the school's reference to go through. That friction is the point.
School detail
Clicking a school opens /platform/schools/$schoolId, a richer screen than the rest of the console. It shows stat tiles, the tenant's config and branding, and tabbed tables of the people inside it — Staff, Students, Guardians, Guests — each its own data table under src/components/data-tables/platform/.
Those people-tables are the one spot in the console that paginate against the server rather than loading everything at once. A shared useCursorParams hook holds the cursor, and each "Next" hands the server the nextCursor from the previous page:
const { params, onSearch, onNext, onPrev, canPrev } = useCursorParams()
// ...
<CursorPaginationFooter
onNext={() => onNext(students?.nextCursor)}
canPrev={canPrev}
/>Cursors, not page numbers
Cursor pagination means you can only step forward and back — there's no "jump to page 9". Search and filters reset you to the first page. This is the platform-console flavor of the data-table story; school-scoped tables elsewhere often load smaller sets in one go.
Curriculum
This is the most conceptually interesting area, so it's worth slowing down. A curriculum version is a reusable template — a national edition like an O-Level or A-Level structure, authored once at the platform level. Schools don't write their own academic structure from scratch; their campuses instantiate the shape (subjects, streams, and so on) from one of these versions. Author the template once, and every tenant inherits a consistent academic skeleton.
The list at /platform/curriculum renders versions as a grid of cards, each tagged with its cycle (O-Level / A-Level) and a status badge — Draft, Active, or Superseded. That status is how an edition is rolled out and later retired without breaking the schools already standing on it.
A version is the root of a small tree. The CurriculumQuery class exposes the whole hierarchy — versions → subjects → themes → topics — with the usual list/get/create/update/delete on each level:
readonly listVersions = async () => this.exec(this.op.listCurriculumVersions())
readonly listSubjects = async (versionId: string) =>
this.exec(this.op.listCurriculumSubjects({ params: { versionId } }))
readonly listThemes = async (subjectId: string) =>
this.exec(this.op.listCurriculumThemes({ params: { subjectId } }))Deleting a version is a cascade — it takes its subjects, themes, and topics with it — so the delete confirm makes you type the edition name, and the query layer invalidates the entire subtree afterward.
Reads are open, writes are gated
Any authenticated user can read the curriculum tree — schools need it to build on. But every write button in this area is wrapped in Can action="manage" resource="curriculum", and the backend enforces the same rule. Reading and editing are two different permissions here. See Permissions.
Operators
The smallest area, and the most self-referential: Operators are the platform admins themselves — accounts with no tenant of their own. The list at /platform/operators is a plain data table of email, name, and created date, with a dialog to create a new one. As with provisioning a school, a freshly created operator signs in via OTP.
One quirk to know about: the listPlatformAdmins endpoint has no OpenAPI schema, so the generated client types its rows as unknown. The route declares a small local Operator type for the handful of fields the table actually reads — a reminder that not every endpoint is fully typed for you.
Where to go next
Permissions
How platform_admin gates the whole console, and the read-vs-manage split on curriculum.
Data Tables
The list-screen toolkit behind every table here, including cursor pagination.
Staff Hub
The school-scoped world that provisioning a tenant hands off to.
The Data Layer
How CurriculumQuery and PlatformQuery talk to the generated client.