Naalya Handbook

Education Hub

The staff, student, guardian and guest web app for the Naalya schools platform — how it is built and how to add to it.

Welcome to the front of the house. The Education Hub is the web application everyone signs into — the staff who run the schools, the students who attend them, the guardians who watch over them, and the guests who haven't enrolled yet. It's the counterpart to the public Website you'd find on Google, and it's powered end-to-end by the API. If you're new here, this section teaches how it's built and how to add to it without surprises.

Five kinds of people share one app, and the app reshapes itself around each of them. After login the platform reads who you are and drops you into your own corner: a guest browsing programs, a student checking grades, a guardian following a child, a staff member running a campus, or a platform_admin overseeing every school at once.

One app, five front doors

There isn't a separate app per audience. There's one TanStack Start app, and the route tree branches by user type — _authenticated/guest, /student, /guardian, /staff-hub, /platform. The user's type (read from the API's /auth/me response) decides which hub they land in. You'll meet that branching in Routing & App Shell.


The stack

Here's the whole thing in one breath, so the vocabulary on later pages isn't a surprise. The Education Hub is a TanStack Start app — React 19 with server-side rendering, built on Vite. The rest of the toolkit is TanStack most of the way down, plus a few deliberate choices:

  • Routing — TanStack Router, file-based out of src/routes/.
  • Data — TanStack Query, using a query-options-factory pattern.
  • Forms — TanStack Form, validated with Zod.
  • Client stateZustand for the things that live in the browser: auth tokens, theme, open side-panels.
  • StylingTailwind 4 with OKLch color tokens, and shadcn/ui built on base-ui primitives — not Radix.
  • Icons — Hugeicons.
  • PermissionsCASL, MongoDB-style ability rules.
  • API client — an Axios client whose types are auto-generated from the API's OpenAPI spec, via chowbea-axios.

Two of those deserve a flag on day one, because they quietly shape almost every file you'll touch.

base-ui, not Radix

The shadcn components here sit on base-ui primitives, so the muscle memory from other shadcn projects is slightly off. The big one: use the render prop for custom triggers, not asChild. Write <DropdownMenuTrigger render={<Button />} />not the asChild form. Routing & App Shell points at where this bites.

The other is the generated client. You never hand-write API calls. The backend publishes an OpenAPI spec, and bun run api:generate turns it into a fully-typed Axios client under src/services/api/_generated/. When the backend changes, you regenerate — and TypeScript tells you what broke. That whole story lives in The Data Layer.


How permissions feel

You don't gate features with if (user.role === ...) scattered around the codebase. Access is governed by CASL — a rules engine that, per user, says which actions they may take on which resources. The rules arrive hydrated from /auth/me, and the resource and action names are themselves derived from the generated API types, so the front-end and back-end can never drift apart on what's gateable:

src/lib/permissions/permissions.types.ts
type ActionType = Extract<AuthMeRuleDto['action'], string>
type ResourceType = Extract<AuthMeRuleDto['subject'], string>

type AppAbility = MongoAbility<[ActionType, ResourceType]>

// the user's type — guest, student, guardian, staff, platform_admin
type UserType = AuthMeUserDto['type']

In a component you reach for <Can> to show or hide; in code you call usePermission(); at the route boundary you call requirePermission() in beforeLoad. You don't need to master this yet — just know it's always there in the background. Permissions is the full tour.


Reading this section

Read it in two passes. On your first day, walk the on-ramp in order — get the app running, learn what the product actually is and who it serves, then learn how a request flows through the router and app shell. That's enough to be useful.

After that, treat the rest as reference. Open The Data Layer when you're fetching or mutating, Permissions when you're gating a feature, or Client State when something needs to live in the browser. Reach for each page when a task takes you there — not all at once.

The code is the source of truth

These pages reshape the team's onboarding notes into something friendlier to learn from, but docs drift over time. When a page and the code disagree, the code wins — and a quick PR to fix the doc is always welcome.


Where to go next

Start at the top and work down — the first three Cards are the first-day path.

On this page