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
| Path | Role |
|---|---|
lumie-frontend/app/page.tsx | Host-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.tsx | Owner onboarding guard and redirects |
lumie-frontend/app/admin/layout.tsx | Staff-only guard and post-login return-path handling |
lumie-frontend/app/dashboard/layout.tsx | Student-only server guard and dashboard shell |
lumie-frontend/app/dashboard/assignments/page.tsx | Student assignment list route mounted at /dashboard/assignments |
lumie-frontend/app/dashboard/assignments/[id]/page.tsx | Student assignment detail route mounted at /dashboard/assignments/[id] |
lumie-frontend/app/auth/callback/page.tsx | Transitional compatibility route that redirects legacy callback links into the password-login modal flow |
lumie-frontend/app/auth/refresh/page.tsx | Silent refresh boundary |
lumie-frontend/app/[customId]/page.tsx | Public academy landing page |
lumie-frontend/app/api/[...path]/route.ts | Same-origin proxy to NEXT_PUBLIC_API_BASE |
lumie-frontend/proxy.ts | Custom-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.ts | Edge-safe custom-domain host, env map, and fallback resolution helpers |
lumie-frontend/src/shared/lib/serverCustomDomain.ts | Server component reader for trusted custom-domain tenant headers |
lumie-frontend/src/shared/config/env.ts | Browser-facing API base. ENV.API_URL is fixed at /api. |
lumie-frontend/src/features/auth/login/ui/LoginForm.tsx | Tenant-aware ID/password login form |
lumie-frontend/src/features/auth/register/ui/RegisterForm.tsx | Student self-registration form used by the shared auth modal on academy-specific entrypoints |
lumie-frontend/src/features/auth/register-owner/ui/OwnerRegisterForm.tsx | Multi-step owner registration flow mounted at /signup |
lumie-frontend/src/entities/session/api/getServerUser.ts | Server-side /v1/me read used by protected layouts |
lumie-frontend/src/entities/session/model/onboarding.ts | Role-based default post-auth redirect logic |
lumie-frontend/src/entities/assignment/api/student-queries.ts | React Query hooks for /v1/assignments/me, /v1/assignments/{id}/me, and PUT /v1/assignments/{id}/me/submission |
lumie-frontend/src/entities/tenant/api/queries.ts | usePublicTenantAuthContext() resolves auth tenant context from server-hydrated tenant data, path custom ID, or custom-domain host |
lumie-frontend/src/entities/tenant/providers/PublicTenantContextHydrator.tsx | Client 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.ts | Client-side tenant custom ID parsing from the pathname |
lumie-frontend/src/shared/lib/tenantRedirect.ts | Auth-boundary login URL builder for custom-domain, canonical custom ID, and root fallback redirects |
lumie-frontend/src/shared/lib/sessionCache.ts | Cached session and tenant redirect context bridge used by shared API code |
lumie-frontend/src/shared/lib/authRedirectReason.ts | Maps backend auth failure codes such as AUTH_017 to URL-safe auth notice reasons |
lumie-frontend/src/shared/api/serverFetch.ts | SSR 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.tsx | Student dashboard navigation entry for /dashboard/assignments |
Route Groups And Major Paths
| Path shape | Purpose | Notes |
|---|---|---|
/ | Public site root or custom-domain tenant landing | Implemented 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, /blog | Canonical public site pages | Implemented under app/(marketing) with shared header and footer |
/login | Authentication entrypoint | Uses the tenant-aware ID/password login form from LoginForm. |
/signup | Owner registration entrypoint | app/(auth)/signup/page.tsx renders OwnerRegisterForm. Student self-registration is exposed through the shared auth modal on tenant-specific entrypoints. |
/onboarding | Owner onboarding flow | Protected in app/(onboarding)/layout.tsx |
/admin/... | Staff and academy operations | Protected in app/admin/layout.tsx |
/dashboard/... | Student experience | Protected in app/dashboard/layout.tsx, then rendered with its own client layout and sidebar shell |
/dashboard/assignments | Student assignment list | Renders StudentAssignmentListPage and fetches GET /v1/assignments/me through the same-origin proxy |
/dashboard/assignments/[id] | Student assignment detail and submit flow | Renders StudentAssignmentDetailPage, reads GET /v1/assignments/{id}/me, and submits with PUT /v1/assignments/{id}/me/submission |
/auth/callback | Transitional compatibility redirect | app/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/refresh | Silent refresh bridge | Used when a refresh cookie exists but an access token must be re-established |
/:customId | Public academy landing page | Implemented with app/[customId]/page.tsx |
/api/[...path] | Same-origin proxy route | Forwards 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
DashboardShellonly after the server session check passes
The assignment redesign adds two student dashboard pages under that shell:
/dashboard/assignmentsfor 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
| Route | UI entrypoint | Backend contract | Notes |
|---|---|---|---|
/dashboard/assignments | app/dashboard/assignments/page.tsx -> StudentAssignmentListPage | GET /v1/assignments/me | Shows 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 -> StudentAssignmentDetailPage | GET /v1/assignments/{id}/me | Reads assignment, canSubmit, optional submission, and optional linked-exam metadata for EXAM_MANUAL. |
/dashboard/assignments/[id] submit action | StudentAssignmentSubmissionForm | PUT /v1/assignments/{id}/me/submission | Renders 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()parsesGET /v1/assignments/meintoStudentAssignment[]and treats the route as a flat list, not a paginated feed.StudentAssignmentListPagederives its "manual assignment" count fromsubmissionMode === 'EXAM_MANUAL'.useStudentAssignment()parsesGET /v1/assignments/{id}/meintoStudentAssignmentDetail.StudentAssignmentSubmissionFormvalidates that each rendered question has a 1-5 answer before calling the student submission mutation.StudentAssignmentResultCardalways renders score, pass/fail, per-question type, and per-question correctness when a graded submission exists. It only depends onshowCorrectAnswersto 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 landinghttps://academy.example/?auth=loginand/?auth=registerkeep auth state on the root URL/login,/register, and/signupredirect to the root query form/admin,/dashboard,/auth/refresh,/api/*, and/favicon.icokeep trusted tenant headers; favicon returns the tenant logo when one exists and falls back to the Lumie favicon otherwise/:customIdand/:customId/loginon the custom domain return404withCache-Control: no-store- unrelated canonical marketing paths on the custom domain return
404 - unconfigured custom domains fail closed with
503outside the bootstrap resolution paths
Auth-boundary redirects also stay tenant-aware. buildTenantLoginUrl(...) uses this precedence:
https://{customDomain}/?auth=loginwhen a verified tenant custom domain is known.https://lumie-edu.com/{customId}?auth=loginwhen only the tenant custom ID is known.https://lumie-edu.com/?auth=loginonly 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
/:customIdresolve 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
/loginand/signuppages 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=/targetopens the shared auth modal?auth=login&reason=session_replacedopens login and shows the replaced-session notice once before strippingreasonfrom 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|allandclass=all|unassigned|<id>; its parser maps them toisActive,classId, orhasActiveEnrollment=falseforGET /v1/students - the exam detail dashboard keeps the same
filterandclassvalues withpage,sort, anddir; it maps them toisActive,classId, orhasActiveEnrollment=falsefor 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()andparseQnaListParams()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-Slugfrom the configured custom-domain host map, or from fallback host resolution for auth write endpoints - rewrites
Set-Cookieheaders 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 localapp/api/health/route.ts - with no auth cookie, the response should be a backend auth response such as
401or403 - with a valid
lumie_access_tokencookie, the same path should return200 - in both cases, a proxied response proves the frontend route exists, stripped spoofable browser-supplied tenant headers, and forwarded to
NEXT_PUBLIC_API_BASEinstead of returning a Next.js404
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.tsandsrc/shared/lib/sessionCache.ts - hits appear in
src/shared/api/base.tsandapp/auth/refresh/RefreshSession.tsx - hits appear in
src/shared/providers/AuthModalProvider.tsxfor the URL-backed notice cleanup