Skip to main content

Architecture

The frontend uses a split architecture: Next.js App Router owns routing, layouts, and server entrypoints in app/, while product code lives in src/ using Feature-Sliced Design.

Layering

The repo-level rule is:

shared -> entities -> features -> widgets -> app/

That rule shows up in the filesystem as follows.

LayerWhat belongs here
src/sharedAPI primitives, env config, generic UI, providers, and cross-cutting utilities
src/entitiesDomain types, schemas, query helpers, generated clients, and entity UI when an entity truly needs it
src/featuresUser-facing actions such as login, onboarding, student management, and report generation
src/widgetsHigher-level compositions such as the landing header, student sidebar, and auth modal
appRoute tree, layouts, loading states, error boundaries, route handlers, and redirects

Root-level exceptions are intentional:

  • components/ui/ contains the forked shadcn primitives
  • hooks/use-mobile.ts and lib/utils.ts stay at the repo root because components.json expects them there

App Router Boundary

app/ is kept thin. Its main responsibilities are:

  • grouping routes with layouts such as (marketing), (auth), and (onboarding)
  • enforcing access control in server layouts such as app/admin/layout.tsx
  • exposing route handlers such as app/api/[...path]/route.ts
  • defining loading and error boundaries close to the route tree

Most business logic is imported from src/ rather than implemented inline in pages. Most authenticated page.tsx files also stay thin and hand off to client feature islands. The server boundary is used more for auth, metadata, and shell concerns than for a blanket authenticated SSR-prefetch rollout.

Data-Access Boundary

The frontend has three distinct access paths:

PathUsed forMain files
Browser fetch through /apiNormal client-side product requestssrc/shared/api/base.ts, src/shared/api/orval-mutator.ts, app/api/[...path]/route.ts
Server fetch during SSR or server layoutsAuth checks and server-only readssrc/shared/api/serverFetch.ts, src/entities/session/api/getServerUser.ts
Public server readsTenant homepage and public academy lookupsentity-level serverFetch helpers used from route pages

The browser always targets ENV.API_URL, which is hardcoded to /api. That keeps auth cookie handling, tenant header propagation, refresh retry, and error normalization in one frontend-controlled boundary.

Source Paths

ContractSource path
Route groups, protected layouts, and route handlerslumie-frontend/app/**
Same-origin proxylumie-frontend/app/api/[...path]/route.ts
Browser fetch primitiveslumie-frontend/src/shared/api/base.ts, lumie-frontend/src/shared/api/orval-mutator.ts
Server auth fetchlumie-frontend/src/entities/session/api/getServerUser.ts, lumie-frontend/src/shared/api/serverFetch.ts
Query client lifecyclelumie-frontend/src/shared/lib/query-client.ts, lumie-frontend/src/shared/providers/QueryProvider.tsx
Session side channel for shared API codelumie-frontend/src/shared/api/sessionAccessor.ts, lumie-frontend/src/shared/lib/sessionCache.ts

Query and Session Architecture

TanStack Query is initialized once in QueryProvider. getQueryClient() creates:

  • a per-request query client on the server to avoid cross-request cache leakage
  • one long-lived browser query client for client navigation

Session state is intentionally split:

  • getServerUser() handles route protection in server layouts
  • useMe() and useMeQuery() expose the authenticated user in client code
  • sessionAccessor plus sessionCache let shared API code read or clear tenant session data without breaking FSD layer rules

UI Composition

The main shells are built as widgets or route-local shells rather than global monolith components:

  • marketing pages use app/(marketing)/layout.tsx
  • admin pages use AdminShell
  • dashboard pages use StudentSidebar, Header, and the shared sidebar primitives

This keeps route-specific layout choices close to their route group while leaving reusable product logic inside src/.

Verification

cd lumie-frontend
rg -n "getServerUser|serverFetch|sessionAccessor|sessionCache|getQueryClient|QueryProvider" \
app src
npx tsc --noEmit

Success means the grep finds the route-protection, server-fetch, session-cache, and query-client boundaries, and npx tsc --noEmit completes without TypeScript errors.