• English
  • Architecture

    This page covers app boot, routing, the auth system, shared HTTP infrastructure, UI preference contexts, shared components, and the deployment model.


    Provider stack (boot order)

    src/main.tsx mounts into #root inside React StrictMode. Providers wrap from outermost to innermost:

    StrictMode
      QueryClientProvider        (TanStack Query — src/lib/queryClient.ts)
        AuthProvider             (session state — src/auth/AuthContext.tsx)
          ThemeProvider          (light/dark/system — src/context/theme-provider.tsx)
            FontProvider         (typeface — src/context/font-provider.tsx)
              DirectionProvider  (LTR/RTL — src/context/direction-provider.tsx)
                RouterProvider   (TanStack Router, routeTree.gen.ts)

    The router is created with:

    • routeTree — auto-generated at src/routeTree.gen.ts by the @tanstack/router-plugin Vite plugin
    • context: { queryClient } — makes the query client available to route loader functions
    • defaultPreload: 'intent' — preloads route chunks on hover/focus
    • defaultPreloadStaleTime: 0

    Routing

    The app uses TanStack Router file-based routing. Route files live in src/routes/; the route tree is auto-generated into src/routeTree.gen.ts on every build/dev start.

    Route tree overview

    __root                             src/routes/__root.tsx
      ├── /_authenticated              Protected layout + auth gate
      │     ├── /                      Overview (fleet KPIs)
      │     ├── /dashboard             Demo scaffold (not live data)
      │     ├── /stores                Store list
      │     ├── /stores/$storeId       Store workspace (tabs)
      │     ├── /health                Health worklist
      │     ├── /tasks                 Tasks worklist
      │     ├── /audit                 Audit log
      │     ├── /operators             Operator management
      │     ├── /roles                 Role management
      │     ├── /help-center/…         Help center
      │     ├── /settings/             User settings (nested layout)
      │     │     ├── /settings/account
      │     │     ├── /settings/appearance
      │     │     ├── /settings/display
      │     │     └── /settings/notifications
      │     └── /errors/$error         In-app error display
      ├── /(auth)                      Auth page group (no URL prefix)
      │     ├── /sign-in
      │     ├── /sign-in-2
      │     ├── /sign-up
      │     ├── /otp
      │     └── /forgot-password
      └── /(errors)
            ├── /401, /403, /404, /500, /503

    Key routing conventions:

    • _authenticated is a pathless layout segment (leading underscore = contributes no URL path). Its route.tsx wraps all children in RequireAuth.
    • (auth) and (errors) are route groups (parentheses = no URL prefix, affects file organization only).
    • __root.tsx creates the root route with createRootRouteWithContext<{ queryClient: QueryClient }>(), enabling loaders to call queryClient.ensureQueryData.
    • __root.tsx renders <NavigationProgress />, <Outlet />, <Toaster /> (sonner), and dev-only devtools.
    • Auto code splitting is enabled (autoCodeSplitting: true in vite.config.ts) — each route is a lazy-loaded chunk.

    The sidebar navigation is defined statically in src/components/layout/data/sidebar-data.ts. Each nav item carries a permission (single, AND) or anyPermission (array, OR) guard. filterNavByPermission() in src/components/layout/filter-nav.ts prunes the nav tree at render time based on the current operator's permissions. firstPermittedPath() resolves where / redirects after login.


    Auth system

    OTP email login flow

    1. Request OTPPOST /v1/ops/auth/request-otp with { email }. Uses raw fetch (not apiFetch) to avoid the auth interceptor. Server always returns 200 (anti-enumeration). Source: src/auth/authApi.ts.

    2. Verify OTPPOST /v1/ops/auth/verify-otp with { email, code }. Returns { access, refresh } (validated as TokenPair).

    3. Store tokenssrc/lib/tokenStore.ts:

      • Access token: in-memory only (let accessToken module variable — never written to storage)
      • Refresh token: localStorage key salon_admin_refresh
      • Email: localStorage key salon_admin_email
    4. Load identityGET /v1/ops/me via apiFetch. Returns { email, displayName?, roles[], permissions[] }. Stored in AuthContext state. Source: src/auth/meApi.ts.

    Session restore on page load

    AuthContext (src/auth/AuthContext.tsx) runs a useEffect on mount:

    1. Reads refresh token from localStorage.
    2. If present: POST /v1/ops/auth/refresh → new TokenPairsetPair + fetchMe → status 'authenticated'.
    3. If missing or refresh fails: clear() → status 'unauthenticated'.

    Silent token refresh (401 handling)

    src/lib/httpClient.ts wraps every authenticated call:

    • On 401: calls refreshOnce() — a singleton in-flight promise so concurrent callers share one refresh request — then retries the original request once with the new access token.
    • On 403: emits onForbidden event → AuthContext re-fetches /v1/ops/me to resync the permission snapshot.
    • If refresh itself fails: emits onSessionDeadAuthContext clears all state → status 'unauthenticated' (user sees login form in-place).

    Route protection

    src/routes/_authenticated/route.tsx renders <RequireAuth> around the layout:

    • loading → centered spinner
    • unauthenticated → renders <LoginPage /> in-place (not a redirect; replaces the child with the login form)
    • authenticated → renders <AuthenticatedLayout />

    Permission checking

    src/auth/RequirePermission.tsx and the can() helper from useAuth():

    • can(action) returns true if roles.includes('admin') (wildcard) or permissions.includes(action).
    • <RequirePermission permission="resource.action"> renders a <ForbiddenError /> fallback (overridable via fallback prop) when the check fails.
    • This is UI-only enforcement — the backend enforces independently.

    Logout

    POST /v1/ops/auth/logout via apiFetch (best-effort), then clear() wipes the access token from memory and refresh from localStorage. Source: src/auth/authApi.ts.


    HTTP client and shared infrastructure

    src/lib/httpClient.ts

    The central authenticated fetch wrapper used by all feature API modules.

    • apiFetch<T>(path, options?) — adds Authorization: Bearer <access> header, auto-retries once on 401, parses JSON, throws ApiError on non-ok responses.
    • apiFetchRaw(path, options?) — same as above but returns the raw Response (used for binary downloads).
    • Emits DOM custom events: onSessionDead, onTokensRotated, onForbidden.
    • Never use raw fetch for authenticated calls. Always use apiFetch.

    src/lib/tokenStore.ts

    Manages the token pair:

    • setPair({ access, refresh }) — stores access in memory, refresh in localStorage.
    • getAccess() / getRefresh() — reads from the respective locations.
    • clear() — wipes both.

    src/lib/queryClient.ts

    Global QueryClient shared across the app:

    • staleTime: 30_000 (30s default)
    • refetchOnWindowFocus only in production
    • Retries only on retryable ApiErrors (502 upstream_error), capped at 2 attempts

    src/lib/config.ts

    • apiUrl(path) — prepends VITE_API_BASE_URL
    • bookingUrl(slug) — constructs VITE_BOOKING_BASE_URL/<slug>

    src/lib/errorMap.ts

    normalizeError(status, body) maps { ok: false, error: "<code>" } responses to operator-friendly message strings for ~25 known error codes (e.g. store_not_found, duplicate_email, invalid_transition).


    UI preference contexts

    All contexts persist preferences in cookies; they wrap the router in the provider stack.

    Context fileManagesPersistence
    src/context/theme-provider.tsxtheme: 'light' | 'dark' | 'system'Cookie vite-ui-theme (1 year). Hard-coded default: 'light'.
    src/context/font-provider.tsxfont: 'inter' | 'manrope' | 'system'Cookie font (1 year).
    src/context/direction-provider.tsxdir: 'ltr' | 'rtl'; sets document.documentElement.dirCookie dir (1 year). Wraps Radix DirectionProvider.
    src/context/layout-provider.tsxSidebar collapsible mode and layout variantCookies layout_collapsible / layout_variant (7 days). Variant hard-coded to 'floating'.
    src/context/search-provider.tsxopen: boolean for the Cmd+K command paletteEphemeral (no cookie).

    These preferences are exposed to users via the Config Drawer (src/components/config-drawer.tsx) — a slide-in settings sheet accessible from the page header, with per-section reset controls and a global reset.


    Shared components

    Layout shell (src/components/layout/)

    FilePurpose
    authenticated-layout.tsxRoot shell — wraps app in SearchProvider, LayoutProvider, SidebarProvider; restores sidebar state from cookie.
    app-sidebar.tsxReads useAuth() for identity, calls filterNavByPermission(), renders nav items via NavGroup.
    header.tsxSticky page header with scroll-aware backdrop blur.
    nav-group.tsxRenders flat links, collapsible sub-menus, and collapsed-sidebar dropdown menus. Handles active-route highlighting.
    nav-user.tsxSidebar footer with operator name/email and sign-out dropdown.

    Data table system (src/components/data-table/)

    A reusable TanStack Table UI kit. The pattern is URL-state-driven:

    URL search params
      → useTableUrlState (deserialize) [src/hooks/use-table-url-state.ts]
        → useReactTable instance
          → DataTable UI components
            → callbacks → navigate() → URL search params updated
    ComponentPurpose
    DataTableToolbarText search, faceted filter chips, reset button, view options
    DataTableFacetedFilterPopover multi-select with per-option counts
    DataTableColumnHeaderSortable column header with Asc/Desc/Hide dropdown
    DataTablePaginationPage selector, rows-per-page, first/prev/next/last buttons
    DataTableBulkActionsFixed floating toolbar for row-selection actions
    DataTableViewOptionsColumn visibility dropdown

    useTableUrlState (src/hooks/use-table-url-state.ts) bridges TanStack Router search params ↔ TanStack Table state. It supports typed column filters ('string' for text, 'array' for multi-select), custom serialize/deserialize, and an ensurePageInRange() guard.

    Other notable shared components

    ComponentNotes
    async-boundary.tsx<AsyncBoundary isPending isError isEmpty> — standardized loading/error/empty states used across all feature components
    confirm-dialog.tsxGeneric confirmation dialog for destructive actions
    responsive-list.tsxThe main table/list primitive used by most features (simpler than full DataTable)
    command-menu.tsxCmd+K command palette
    config-drawer.tsxFull theme/layout settings sheet (see UI preference contexts)
    pager.tsxSimple offset-based pagination used with ResponsiveList

    Zustand store (legacy)

    src/stores/auth-store.ts defines useAuthStore, which stores user info and an access token in a cookie (cookie name: 'thisisjustarandomstring'). This store is not used by the active auth flow. The live system uses AuthContext + tokenStore.ts. The store is a leftover from an earlier implementation and can be treated as dead code.


    Deployment

    The app is a static SPA deployed to Cloudflare Workers via wrangler. Configuration: wrangler.jsonc.

    EnvironmentCommandDomainWrangler env
    Productionnpm run deployconsole.auralynkai.comdefault
    UATnpm run deploy:uatconsole-uat.auralynkai.comuat

    Key wrangler settings:

    • assets.not_found_handling: single-page-application — SPA fallback routing (all 404s serve index.html)
    • compatibility_flags: [nodejs_compat]
    • observability.enabled: true

    A netlify.toml also exists, suggesting Netlify was a previous or considered alternative deploy target.

    There is no CI/CD pipeline — deploys are currently run manually from a developer machine.


    Changing this area

    • Routing changes: Edit files in src/routes/. The route tree auto-regenerates. Do not edit src/routeTree.gen.ts by hand.
    • Auth flow changes: Touch src/auth/AuthContext.tsx, src/auth/authApi.ts, and src/lib/httpClient.ts. Update tests in src/test/auth/ and src/test/lib/httpClient*.test.ts.
    • New env var: Add to .env.example, .env.development, .env.uat, .env.production, and src/lib/config.ts. Never commit secret values.
    • Nav + permissions: Update src/components/layout/data/sidebar-data.ts and ensure the permission string exists in the server-side permission catalog.