Skip to main content

Routing

This page is a reference for the frontend routing surface. Lumie uses the Next.js App Router exclusively: route groups organize the major user surfaces, layouts enforce access, and app/api/[...path]/route.ts is the same-origin proxy that all browser-side API calls traverse.

Source Paths

PathRole
lumie-frontend/app/page.tsxHost-native site root. Canonical hosts render the Lumie marketing home; trusted custom domains render the tenant landing at /.
lumie-frontend/app/(marketing)/Public canonical marketing pages such as /about, /pricing, /features, and /blog
lumie-frontend/app/(auth)/Login and owner-signup entrypoints
lumie-frontend/app/(onboarding)/layout.tsxOwner onboarding guard and redirects
lumie-frontend/app/admin/layout.tsxStaff-only guard and post-login return-path handling
lumie-frontend/app/dashboard/layout.tsxStudent-only server guard and dashboard shell
lumie-frontend/app/dashboard/assignments/page.tsxStudent assignment list route mounted at /dashboard/assignments
lumie-frontend/app/dashboard/assignments/[id]/page.tsxStudent assignment detail route mounted at /dashboard/assignments/[id]
lumie-frontend/app/auth/callback/page.tsxTransitional compatibility route that redirects legacy callback links into the password-login modal flow
lumie-frontend/app/auth/refresh/page.tsxSilent refresh boundary
lumie-frontend/app/[customId]/page.tsxPublic academy landing page
lumie-frontend/app/api/[...path]/route.tsSame-origin proxy to NEXT_PUBLIC_API_BASE
lumie-frontend/proxy.tsCustom-domain host pinning, root auth URL normalization, fail-closed custom-domain routing, and auth routing gates before the route tree renders
lumie-frontend/src/shared/lib/customDomain.tsEdge-safe custom-domain host, env map, and fallback resolution helpers
lumie-frontend/src/shared/lib/serverCustomDomain.tsServer component reader for trusted custom-domain tenant headers
lumie-frontend/src/shared/config/env.tsBrowser-facing API base. ENV.API_URL is fixed at /api.
lumie-frontend/src/features/auth/login/ui/LoginForm.tsxTenant-aware ID/password login form
lumie-frontend/src/features/auth/register/ui/RegisterForm.tsxStudent self-registration form used by the shared auth modal on academy-specific entrypoints
lumie-frontend/src/features/auth/register-owner/ui/OwnerRegisterForm.tsxMulti-step owner registration flow mounted at /signup
lumie-frontend/src/entities/session/api/getServerUser.tsServer-side /v1/me read used by protected layouts
lumie-frontend/src/entities/session/model/onboarding.tsRole-based default post-auth redirect logic
lumie-frontend/src/entities/assignment/api/student-queries.tsReact Query hooks for /v1/assignments/me, /v1/assignments/{id}/me, and PUT /v1/assignments/{id}/me/submission
lumie-frontend/src/entities/tenant/api/queries.tsusePublicTenantAuthContext() resolves auth tenant context from server-hydrated tenant data, path custom ID, or custom-domain host
lumie-frontend/src/entities/tenant/providers/PublicTenantContextHydrator.tsxClient bridge that exposes the server-resolved public tenant from /:customId pages and custom-domain root rendering to the shared auth modal
lumie-frontend/src/entities/tenant/lib/useTenantCustomIdFromPath.tsClient-side tenant custom ID parsing from the pathname
lumie-frontend/src/shared/lib/tenantRedirect.tsAuth-boundary login URL builder for custom-domain, canonical custom ID, and root fallback redirects
lumie-frontend/src/shared/lib/sessionCache.tsCached session and tenant redirect context bridge used by shared API code
lumie-frontend/src/shared/lib/authRedirectReason.tsMaps backend auth failure codes such as AUTH_017 to URL-safe auth notice reasons
lumie-frontend/src/shared/api/serverFetch.tsSSR fetch boundary that loops back through /api
lumie-frontend/src/features/assignment-management/list-student-assignments/Student assignment list cards and metrics for /dashboard/assignments
lumie-frontend/src/features/assignment-management/view-student-assignment/Student assignment detail, submit, and result components
lumie-frontend/src/widgets/student-sidebar/ui/StudentSidebar.tsxStudent dashboard navigation entry for /dashboard/assignments

Route Groups And Major Paths

Path shapePurposeNotes
/Public site root or custom-domain tenant landingImplemented in app/page.tsx. Canonical hosts render the Lumie marketing home with shared header and footer; trusted custom domains render the tenant landing without the Lumie marketing chrome.
Marketing paths such as /about, /pricing, /features, /blogCanonical public site pagesImplemented under app/(marketing) with shared header and footer
/loginAuthentication entrypointUses the tenant-aware ID/password login form from LoginForm.
/signupOwner registration entrypointapp/(auth)/signup/page.tsx renders OwnerRegisterForm. Student self-registration is exposed through the shared auth modal on tenant-specific entrypoints.
/onboardingOwner onboarding flowProtected in app/(onboarding)/layout.tsx
/admin/...Staff and academy operationsProtected in app/admin/layout.tsx
/dashboard/...Student experienceProtected in app/dashboard/layout.tsx, then rendered with its own client layout and sidebar shell
/dashboard/assignmentsStudent assignment listRenders StudentAssignmentListPage and fetches GET /v1/assignments/me through the same-origin proxy
/dashboard/assignments/[id]Student assignment detail and submit flowRenders StudentAssignmentDetailPage, reads GET /v1/assignments/{id}/me, and submits with PUT /v1/assignments/{id}/me/submission
/auth/callbackTransitional compatibility redirectapp/auth/callback/page.tsx is a tombstone route for legacy links. It validates callbackUrl and redirects to password login, not into an active OAuth or SSO completion flow.
/auth/refreshSilent refresh bridgeUsed when a refresh cookie exists but an access token must be re-established
/:customIdPublic academy landing pageImplemented with app/[customId]/page.tsx
/api/[...path]Same-origin proxy routeForwards browser requests to the backend gateway

The root route intentionally lives at app/page.tsx so custom domains can render tenant landings at / without making every canonical marketing page dynamic. Canonical marketing subpages remain under app/(marketing).

/api/health is a separate local route in app/api/health/route.ts. Use it for frontend liveness only; it does not exercise the backend proxy.

Access Control

Route protection happens in layouts, not scattered across client components.

Onboarding

app/(onboarding)/layout.tsx:

  • fetches the current user with getServerUser()
  • redirects anonymous users to login or session refresh
  • redirects students to /dashboard
  • redirects already-onboarded owners to /admin

Admin

app/admin/layout.tsx:

  • checks the current user on the server
  • preserves the requested admin path for post-login return
  • redirects students to /dashboard
  • redirects not-yet-onboarded owners to /onboarding

Dashboard

app/dashboard/layout.tsx:

  • checks the current user on the server
  • preserves the requested dashboard path for post-login return
  • redirects anonymous users to login or session refresh
  • redirects non-students to /admin
  • renders the client DashboardShell only after the server session check passes

The assignment redesign adds two student dashboard pages under that shell:

  • /dashboard/assignments for the targeted assignment list;
  • /dashboard/assignments/[id] for detail, manual answer submission, and immediate result display.

Both routes are reachable from StudentSidebar, which now includes a dedicated /dashboard/assignments menu item.

Student Assignment Routes

RouteUI entrypointBackend contractNotes
/dashboard/assignmentsapp/dashboard/assignments/page.tsx -> StudentAssignmentListPageGET /v1/assignments/meShows the current student's visible assignments only. The page derives summary counts from the returned list and does not SSR prefetch.
/dashboard/assignments/[id]app/dashboard/assignments/[id]/page.tsx -> StudentAssignmentDetailPageGET /v1/assignments/{id}/meReads assignment, canSubmit, optional submission, and optional linked-exam metadata for EXAM_MANUAL.
/dashboard/assignments/[id] submit actionStudentAssignmentSubmissionFormPUT /v1/assignments/{id}/me/submissionRenders a 1-5 objective-answer choice grid for the linked exam sheet, then invalidates both the list and detail queries after success.

The detail route is parameter-only. It does not call notFound() at the page boundary. Non-positive or non-numeric IDs are parsed by the client route layer and leave the assignment query disabled, so those paths render the frontend error state instead of calling GET /v1/assignments/{id}/me.

Student Assignment UI Behavior

  • useStudentAssignments() parses GET /v1/assignments/me into StudentAssignment[] and treats the route as a flat list, not a paginated feed.
  • StudentAssignmentListPage derives its "manual assignment" count from submissionMode === 'EXAM_MANUAL'.
  • useStudentAssignment() parses GET /v1/assignments/{id}/me into StudentAssignmentDetail.
  • StudentAssignmentSubmissionForm validates that each rendered question has a 1-5 answer before calling the student submission mutation.
  • StudentAssignmentResultCard always renders score, pass/fail, per-question type, and per-question correctness when a graded submission exists. It only depends on showCorrectAnswers to decide whether to reveal answer keys.

That means the frontend routing contract for the detail page is coupled to the assignment service's student-specific response shape, not to the staff submission APIs.

Transitional Compatibility Route

app/auth/callback/page.tsx still exists so old bookmarks or external redirects that target /auth/callback?callbackUrl=... do not fall straight into a 404.

The page is not an OAuth provider callback and does not exchange any provider code or token. Its only job is to sanitize the relative callbackUrl and redirect the browser to a password-login entrypoint:

  • missing or invalid callback URLs go to /login
  • safe generic callback URLs go to /login?callbackUrl=...
  • tenant callback URLs go to /:customId?auth=login&callbackUrl=...

That makes /auth/callback a decommission shim for the removed SSO flow, not part of the active sign-in contract.

Tenant-Aware Routing

Two tenant-aware public URL patterns exist today:

  • canonical tenant URLs under https://lumie-edu.com/:customId
  • host-native custom domains such as https://academy.example/

Canonical tenant URLs keep the tenant custom ID in the path and are rendered by app/[customId]/page.tsx. Custom domains do not expose the custom ID path in the public URL. proxy.ts pins trusted tenant headers from CUSTOM_DOMAIN_TENANT_MAP or the transitional by-domain lookup, then lets app/page.tsx render the tenant landing directly at /.

Configured custom domains use this env-map shape. Source anchors: src/shared/lib/customDomain.ts#resolveConfiguredCustomDomainContext and lumie-infra/applications/lumie/frontend/CUSTOM_DOMAIN_RUNBOOK.md#setup.

CUSTOM_DOMAIN_TENANT_MAP=academy.example=<SLUG>:<CUSTOM_ID>

<SLUG> is the backend tenant identity used for API and auth pinning. <CUSTOM_ID> is the public canonical path alias used by lumie-edu.com/:customId. They may differ. To avoid split identity bugs, app/page.tsx fetches public tenant data by <CUSTOM_ID> and fails closed unless the fetched tenant slug equals the trusted <SLUG>.

Custom-domain routing is host-native:

  • https://academy.example/ renders the tenant landing
  • https://academy.example/?auth=login and /?auth=register keep auth state on the root URL
  • /login, /register, and /signup redirect to the root query form
  • /admin, /dashboard, /auth/refresh, /api/*, and /favicon.ico keep trusted tenant headers; favicon returns the tenant logo when one exists and falls back to the Lumie favicon otherwise
  • /:customId and /:customId/login on the custom domain return 404 with Cache-Control: no-store
  • unrelated canonical marketing paths on the custom domain return 404
  • unconfigured custom domains fail closed with 503 outside the bootstrap resolution paths

Auth-boundary redirects also stay tenant-aware. buildTenantLoginUrl(...) uses this precedence:

  1. https://{customDomain}/?auth=login when a verified tenant custom domain is known.
  2. https://lumie-edu.com/{customId}?auth=login when only the tenant custom ID is known.
  3. https://lumie-edu.com/?auth=login only when no tenant URL information is available.

Source anchors: src/shared/lib/tenantRedirect.ts, src/shared/lib/sessionCache.ts, and src/shared/api/base.ts.

Auth forms do not rely on pathname parsing alone. app/[customId]/page.tsx and custom-domain rendering in app/page.tsx resolve the public tenant on the server, then mount PublicTenantContextHydrator so the shared auth modal can read that verified tenantSlug before browser-side public tenant lookup runs. usePublicTenantAuthContext() prefers that server-hydrated tenant context, then falls back to useTenantCustomIdFromPath() plus public tenant lookup by path custom ID or custom-domain host. It returns the active tenantSlug, tenantCustomId, and whether the context came from server, path, or host.

That shared auth context keeps three browser entry cases aligned:

  • branded landing pages under /:customId resolve tenant context on the server and hydrate it into the shared auth modal
  • custom-domain root pages resolve tenant context from trusted host headers and hydrate it into the shared auth modal
  • generic /login and /signup pages stay tenantless until the browser is on a tenant-specific path or host

When tenant context is available, the shared auth modal can expose student self-registration without hardcoding academy identity into the generic /login or /signup pages.

Tenant-scoped login remains fail-closed in the frontend until a tenantSlug is resolved. The password-login request may still include the URL customId, but authenticated session state and subsequent API calls trust the backend-issued tenantSlug and tenantId, not the public alias.

When a session expires at an auth boundary, sessionCache.getTenantRedirectContext() first uses current-tenant query data when it is warm, then reads backend-verified tenantCustomId and tenantCustomDomain from the cached /v1/me user. If neither cache exists, it falls back to the current browser host or the first non-reserved path segment so a cold 401 on /:customId or a custom-domain host does not collapse to the root login page.

Query Parameters As Route State

Some UI state is intentionally encoded in the URL:

  • ?auth=login|register&callbackUrl=/target opens the shared auth modal
  • ?auth=login&reason=session_replaced opens login and shows the replaced-session notice once before stripping reason from the URL
  • list pages such as students and Q&A keep filter, search, sort, and page state in search params
  • the student list uses filter=active|inactive|all and class=all|unassigned|<id>; its parser maps them to isActive, classId, or hasActiveEnrollment=false for GET /v1/students
  • the exam detail dashboard keeps the same filter and class values with page, sort, and dir; it maps them to isActive, classId, or hasActiveEnrollment=false for every exam-detail statistics view, including summary, grades, class comparison, item analysis, and choice distribution
  • the learning report dashboard keeps the selected class filter in ?classId=<id> and removes the key for the all-classes view
  • helper parsers such as parseStudentListParams() and parseQnaListParams() keep URL parsing aligned between route code and query keys

This makes list views bookmarkable and helps TanStack Query use stable keys for the same route state. The report dashboard also clears classId when the selected exam changes, so a class chosen for one exam cannot hide rows for the next exam. Report generation actions on that dashboard operate on the currently visible selected rows after all active dashboard filters are applied.

Proxy Route Behavior

app/api/[...path]/route.ts is part of the routing surface even though it does not render UI. It:

  • forwards requests to NEXT_PUBLIC_API_BASE
  • removes browser-only or unsafe forwarded headers such as origin
  • strips spoofable tenant and user headers from the browser request
  • attaches X-Tenant-Slug from the configured custom-domain host map, or from fallback host resolution for auth write endpoints
  • rewrites Set-Cookie headers for localhost proxy mode
  • strips stale compression headers after Node fetch decompression

Source anchor: lumie-frontend/app/api/[...path]/route.ts#proxyRequest

const host = normalizeHost(request.headers.get('host'));
const targetUrl = `${getApiBase()}${url.pathname}${url.search}`;
const headers = new Headers(request.headers);
headers.delete('host');
headers.delete('x-tenant-slug');
headers.delete('x-lumie-custom-id');
headers.delete('x-tenant-id');
headers.delete('x-user-id');
headers.delete('x-user-role');
headers.delete('origin');

if (isResolutionEnabled() && isCustomDomainCandidate(host)) {
const configured = resolveConfiguredCustomDomainContext(host);
if (configured) {
headers.set('x-tenant-slug', configured.slug);
} else if (isAuthWriteEndpoint(url.pathname)) {
const tenant = await resolveCustomDomainTenant(host);
if (!tenant?.slug) return customDomainUnavailableResponse();
headers.set('x-tenant-slug', tenant.slug);
} else {
return customDomainUnavailableResponse();
}
}

That same handler also rewrites Set-Cookie by removing Domain and Secure, then downgrading SameSite=None to SameSite=Lax so auth cookies can land on http://localhost:3000 during local development.

Verifiable Proxied Request

Run this from the frontend repo while next dev is serving http://localhost:3000 and NEXT_PUBLIC_API_BASE points at a reachable backend:

cd lumie-frontend
curl -i http://localhost:3000/api/v1/me

Expected success signal:

  • the request is handled by app/api/[...path]/route.ts, not by the local app/api/health/route.ts
  • with no auth cookie, the response should be a backend auth response such as 401 or 403
  • with a valid lumie_access_token cookie, the same path should return 200
  • in both cases, a proxied response proves the frontend route exists, stripped spoofable browser-supplied tenant headers, and forwarded to NEXT_PUBLIC_API_BASE instead of returning a Next.js 404

Static Wiring Check For Student Assignment Routes

cd /path/to/Lumie
rg -n "dashboard/assignments|useStudentAssignments|useStudentAssignment|useSubmitStudentAssignment" \
lumie-frontend/app/dashboard \
lumie-frontend/src/features/assignment-management \
lumie-frontend/src/entities/assignment/api/student-queries.ts \
lumie-frontend/src/widgets/student-sidebar/ui/StudentSidebar.tsx

Expected success signal:

  • hits appear for app/dashboard/assignments/page.tsx
  • hits appear for app/dashboard/assignments/[id]/page.tsx
  • hits appear for the student query hooks and sidebar navigation item

Static Wiring Check For Auth-Boundary Redirects

cd /path/to/Lumie
rg -n "buildTenantLoginUrl|getTenantRedirectContext|SESSION_REPLACED_BY_LOGIN_CODE|session_replaced" \
lumie-frontend/src/shared \
lumie-frontend/app/auth \
lumie-frontend/src/features/auth/logout

Expected success signal:

  • hits appear in src/shared/lib/tenantRedirect.ts and src/shared/lib/sessionCache.ts
  • hits appear in src/shared/api/base.ts and app/auth/refresh/RefreshSession.tsx
  • hits appear in src/shared/providers/AuthModalProvider.tsx for the URL-backed notice cleanup