PVR Tech Studio
Dev architecture

Architecture & Routing

How the Luminaux app boots, how routes are defined, and how state and persistence are organized.

5 min read
Updated July 15, 2026

The application has three architectural pillars, and understanding them makes the rest of the codebase predictable:

  1. A provider tree (src/main.tsx) that supplies layout config, motion config, and toasts.
  2. A single routing source of truth (src/routes.tsx) that also declares per-route presentation.
  3. A file-per-feature data layer (src/data/*.ts) with a consistent seed + load/save/clear persistence pattern.

The provider tree

src/main.tsx mounts the app under StrictMode with this nesting:

// src/main.tsx (shape)
createRoot(document.getElementById('root')!).render(
    <StrictMode>
        <BrowserRouter>
            <LayoutProvider>       {/* shell config, skin, OLED — src/context/LayoutContext.tsx */}
                <MotionProvider>   {/* global reduced-motion config — src/components/motion */}
                    <ToastProvider> {/* toast state — src/components/ui/Toast */}
                        <App />
                        <AppSplash />   {/* branded first-load splash */}
                        <Toaster />     {/* mounted at root so toasts survive route changes */}
                    </ToastProvider>
                </MotionProvider>
            </LayoutProvider>
        </BrowserRouter>
    </StrictMode>,
)

Key point: <Toaster /> lives at the root, not inside AppLayout, so toasts persist across navigation (e.g. a sign-out toast that outlives the route change). ChatProvider is mounted lower, inside AppLayout, because the chat widgets only exist within the shell.

Pre-paint scripts (no flash)

index.html contains an inline script that runs before React, reading localStorage to apply the correct theme/skin/language on the very first paint (avoiding a flash of the wrong theme):

  • adds .dark to <html> when theme === 'dark' (or system + OS dark);
  • validates and sets data-skin (falls back to default if the saved value isn't in the allowlist);
  • sets data-oled when oled_dark === 'true';
  • sets <html lang> from the saved language (whitelisted to the supported languages).

Because these attributes are set pre-paint, the token system resolves the right colors immediately.

The routing model

src/routes.tsx is the single source of truth for real pages. It exports a typed array:

// src/routes.tsx (shape)
export interface AppRoute {
    path: string
    element: React.ReactNode
    fullBleed?: boolean     // drop <main> padding; page owns its height
    headerOnly?: boolean    // hide the sidebar; nav moves to the header
    fixedFooter?: boolean   // pin the footer
    hideWidgets?: boolean   // suppress chat widgets on this route
}
 
export const appRoutes: AppRoute[] = [
    {path: '/dashboards/analytics', element: <AnalyticsDashboard />},
    {path: '/apps/chat', element: <ChatPage />, fullBleed: true, headerOnly: true, fixedFooter: true},
    {path: '/ui/panels', element: <PanelsPage />},
    // …~90 entries, including two data-driven folds (see below)
]

Two route groups are folded in from data arrays instead of being hand-listed, so their source of truth stays with the feature:

  • Page-Layout previews...LAYOUT_PREVIEWS.map(...) turns the preview descriptor array (src/pages/layouts/LayoutPreviewPage.tsx) into 20 /page-layouts/* routes, all rendering the shared <LayoutPreviewPage variant={…}/> template.
  • Email templates...EMAIL_TEMPLATES.map(...) turns the descriptor array in src/data/emailTemplates/meta.ts into 12 lazy, full-bleed /email/* routes. The raw email HTML ships in that page's lazy chunk, not the main bundle.

src/App.tsx then:

  • maps appRoutes to <Route> elements (lazy ones wrapped in <Suspense fallback={<PageLoader/>}>);
  • renders the skin-dependent / index from its own module: HomeRoute() returns <BentoDashboard/> when the skin is bento, <ConsoleDashboard/> when console, else <CrmDashboard/>;
  • splits the flattened menu leaves into shell leaves (anything not covered by appRoutes falls back to a titled <Placeholder>) and auth leaves (/auth/*), which render outside the <AppLayout> route.

Standalone auth routes (authRoutes)

Auth pages render standalone — outside the AppLayout shell (no sidebar/header/footer), so they are not in appRoutes. src/routes.tsx exports a second map:

// src/routes.tsx (shape)
export const authRoutes: Record<string, ReactNode> = {
    '/auth/login': <LoginPage />,
    '/auth/login-v2': <LoginV2Page />,
    '/auth/register': <RegisterPage />,
    '/auth/register-v2': <RegisterV2Page />,
    '/auth/forgot-password': <ForgotPasswordPage />,
    '/auth/reset-password': <ResetPasswordPage />,
    '/auth/lock-screen': <LockScreenPage />,
}

App.tsx maps each /auth/* menu leaf to authRoutes[l.to] ?? <Placeholder …/> as a sibling of the <AppLayout> route — so an auth page owns the whole viewport, and any future /auth/* menu leaf without an entry still falls back to a standalone placeholder. Paths must match the menu (src/data/menu.ts). The seven screens are covered in Authentication.

Per-route presentation flags

The presentation flags on an appRoutes entry are surfaced through useRouteLayout() (src/layout/routeLayout.ts), which builds a routePresentation map and lets the shell force a layout for specific routes without touching the user's saved config. Examples in the codebase:

  • /apps/chatfullBleed + headerOnly + fixedFooter (the chat page owns the whole viewport and supplies its own rail).
  • /apps/pricingfullBleed + headerOnly + hideWidgets (a marketing landing page — hideWidgets suppresses the chat rail/bubble there).
  • /apps/scrumboard, /apps/calendar, /apps/contactsfullBleed.
  • The 12 /email/* template routes and the 3 immersive /error/* pages → fullBleed.

useRouteLayout() returns {forceHeaderOnly, forceFixedFooter, forceHideWidgets, fullBleed}, consumed by AppLayout and Header. Full-bleed pages do not render <PageHeader> (they call useDocumentTitle directly) and size themselves with calc(100dvh - var(--app-header-height) - <footer>).

Placeholders & the catch-all

The placeholder mechanism keeps the menu and the router from drifting. App.tsx computes the difference:

// Conceptually:
const explicit = new Set(['/', ...appRoutes.map(r => r.path)])
// Every menu leaf whose `to` is NOT explicit renders a titled <Placeholder> instead —
// inside the shell for normal routes, standalone for /auth/* routes.

As of today, every menu leaf resolves to a real page — the mechanism stays as a safety net: add a menu leaf before its route exists and it renders a friendly titled "coming soon" screen (src/pages/Placeholder.tsx) instead of breaking navigation. A catch-all <Route path="*"> renders a pages:pageNotFound placeholder (the designed, immersive 404 lives at /error/404).

The data layer

Feature data lives in src/data/<feature>.ts, each following the same shape:

// src/data/<feature>.ts (pattern)
export const STORAGE_KEY = 'feature-state-v1'
export interface FeatureState { /* … */ }
export const seed: FeatureState = { /* … */ }
 
export function loadX(): FeatureState { /* read localStorage, fall back to seed */ }
export function saveX(state: FeatureState): void { /* write localStorage */ }
export function clearX(): void { /* remove the key */ }

Modules that follow this: chat.ts (chat-state-v2), calendar.ts (calendar-state-v2), scrumboard.ts (scrumboard-state-v1), contacts.ts (contacts-state-v1), invoice.ts (invoice-state-v1), cookies.ts (cookie-consent-v1). Static (non-persisted) data: menu.ts, navBadges.ts, apps.ts, quickCreate.ts, megaMenu.ts, user.ts, notifications.ts, pricing.ts, settings.ts, faq.ts, countries.ts, and the emailTemplates/ folder.

Feature view-models are hooks. Large app pages keep state/logic in a co-located hook and stay render-only: useBoard (scrumboard), useCalendar (calendar), useApplicants (contacts), useChatPanes/useListMode (chat). Follow this pattern for new complex pages.

Persistence & Reset

Every persisted key is registered in src/lib/appStorage.ts:

// src/lib/appStorage.ts (shape)
export const APP_LOCAL_KEYS = [
    'layout_config', 'design_skin', 'oled_dark', 'theme', 'language',
    'nav-favorites', 'user-status', 'recent-routes',
    'chat-rail-collapsed', 'chat-state-v2', 'chat-list-mode',
    'emoji-mart.frequently', 'emoji-mart.last',
    'contacts-state-v1', 'scrumboard-state-v1', 'calendar-state-v2',
    'invoice-state-v1', 'cookie-consent-v1',
    // + a few legacy keys kept so Reset also cleans up older versions' state
] as const
export const APP_SESSION_KEYS = ['app-splash-shown'] as const
 
export function clearAppStorage() { /* removes every key above — but NOT auth_token */ }

"Reset to defaults" (in the Customizer + Layout Settings) calls clearAppStorage() then reloads, so every store re-hydrates from defaults + seed while the user stays signed in (auth_token is deliberately preserved).

Best practices

  • Add real pages via appRoutes — never hand-maintain a parallel list of "real" routes.
  • Keep page components render-only; push state into a co-located useX hook when it grows.
  • Force per-route layout with presentation flags, not by mutating LayoutContext.
  • Register every new localStorage / sessionStorage key in appStorage.ts.

Troubleshooting

SymptomCause / fix
A menu item shows "coming soon"Its to has no appRoutes (or authRoutes) entry — add the route to make it real.
A stuck boolean ref in dev onlyStrictMode double-mount — set the ref true on mount, not just false on cleanup.
Reset didn't clear my new feature's stateThe key isn't in appStorage.ts. Add it.
Full-bleed page has a scroll gapSize it with calc(100dvh - var(--app-header-height) - <footer>); subtract the footer only when fixedFooter.

Was this page helpful?