InfrastructureSPA Architecture

Frontend

Frontend responsibilities, component tree, module registration, routing, build pipeline, and CI/CD for the SPA.

👤 Aakash Pawar📅 Updated: Apr 20, 2026📁 SPA Architecture

Frontend

What Frontend Owns

ConcernFrontend responsibility
Provider stackInitialize and share Redux, i18n, LaunchDarkly, Sentry, AG Grid, ErrorBoundary across all modules — once per SPA session
Module registrationEach module exports useXxxRoutes() and useXxxSidebarMenu() hooks consumed by the unified Routes component
Module resolutionDetermine the active module from the URL path via matchingModule logic
Route renderingRender only the active module's routes in the Switch, with AuthRoute permission checks
SidebarRender a unified sidebar with the active module's menu items and a module switcher dropdown
SPA bootstrapEager-load all module settings (session, operations, finance, registration, CRM, inventory) during initial mount
Code splittingUse React.lazy() + Suspense for per-route code splitting
Spotlight searchCmd+K cross-module search integration
Build pipelineWebpack configuration for SPA, single-build, and all-modules build modes

Component Tree

Entry Point: CrelioDashboard/index.tsx

Source: src/modules/CrelioDashboard/index.tsx

The SPA entry point renders <Main> with its <Routes>:

import ReactDOM from "react-dom";
import { Main } from "../../index";
import { Routes } from "./RoutesAndMenus/routes";
import "src/stylesheets/viewport.scss";

const Container = () => {
  return <Main Wrapped={<Routes />} />;
};

ReactDOM.render(<Container />, document.getElementById("root"));

The difference from standalone modules is that <Routes> here contains all module routes, not just a single module's routes.

Main — The Provider Stack

Source: src/index.tsx

Main is shared across all modules (SPA and standalone). It initializes:

ProviderPurpose
SuspenseFallback loading spinner for React.lazy() components
LDProviderLaunchDarkly feature flag context
Provider (Redux)Global state store
I18nextProviderInternationalization
ErrorBoundarySentry error boundary for crash reporting
RouterReact Router with shared history object
AppAlertGlobal toast notification system
DetectNetworkOffline/online detection banner

All of these are initialized once when the SPA loads and remain alive across module switches.

Module Registration Pattern

Each module contributes to the SPA via two custom hooks:

HookReturnsPurpose
useXxxRoutes()CrelioRouteType[]Array of route definitions (path + component + props)
useXxxSidebarMenu()CrelioSidebarItem[]Array of sidebar menu items for the module

Example: Operations Module

Source: RoutesAndMenus/operationsRoutesAndMenu.tsx

// Routes hook
export const useOperationsRoutes = (): CrelioRouteType[] => {
  return [
    { path: "/operation", Component: OperationsDashboard },
    { path: "/operation/operation-dashboard/:route", Component: OperationsDashboard },
    { path: "/operation/testwise-waiting-list/:route", Component: WaitingList },
    // ... more routes
  ];
};

// Sidebar hook
export const useOperationsSidebarMenu = (): CrelioSidebarItem[] => {
  return [
    { name: t("Operations Dashboard"), icon: "fa-chart-line", route: "/operation" },
    { name: t("Waiting List"), icon: "fa-list", route: "/operation/testwise-waiting-list/all-tests" },
    // ... more menu items
  ];
};

All Registered Modules

These hooks are consumed in routes.tsx and assembled into a modules array:

const modules: Module[] = useMemo(() => [
  { menu: financeSidebarMenu, routes: financeRoutes, selectedModule: t("Finance") },
  { menu: labAdminSidebarMenu, routes: labAdminRoutes, selectedModule: t("Admin") },
  { menu: accessionSidebarMenu, routes: accessionRoutes, selectedModule: t("Accession") },
  { menu: inventorySidebarMenu, routes: inventoryRoutes, selectedModule: t("Inventory") },
  { menu: operationSidebarMenu, routes: operationsRoutes, selectedModule: t("Operation") },
  { menu: registrationSidebarMenu, routes: registrationRoutes, selectedModule: t("Registration") },
  { menu: crmDashboardSidebarMenu, routes: crmDashboardRoutes, selectedModule: t("CRM") },
], [/* dependencies */]);

Module Resolution — matchingModule

Source: RoutesAndMenus/routes.tsx

The SPA determines which module is active purely from the URL:

const matchingModule: Module = useMemo(() => {
  const pathParts: string[] = location.pathname.split("/").filter(Boolean);
  const moduleName: string = pathParts[0]?.toLowerCase();

  // Strategy 1: Match by sidebar route
  const moduleByRoute: Module | undefined = modules.find((module: Module) =>
    module.menu.some((item: CrelioSidebarItem) => {
      if (item.route) {
        const itemRouteBase = item.route.split("/").filter(Boolean)[0]?.toLowerCase();
        const locationPathBase = pathParts[0]?.toLowerCase();
        const isExactMatch = location.pathname === item.route;
        const isBaseMatch = locationPathBase === itemRouteBase;
        return isExactMatch || isBaseMatch;
      }
      return false;
    })
  );

  // Strategy 2: Match by module name
  const moduleByName: Module | undefined = modules?.find(
    (module: Module) => module.selectedModule?.toLowerCase() === moduleName
  );

  // Fallback: Registration
  return moduleByRoute || moduleByName || {
    menu: registrationSidebarMenu,
    routes: registrationRoutes,
    selectedModule: t("Registration"),
  };
}, [location.pathname, modules, t]);

Resolution Priority

PriorityStrategyExample
1Route match — first path segment matches a sidebar item's route base/finance/dashboard → Finance module
2Name match — first path segment matches a module's selectedModule/operation → Operation module
3Fallback — if nothing matches, default to Registration/unknown-path → Registration module

Route Type System

Source: CrelioDashboard/routeTypes.ts

Core Types

// A regular route with a component
type CrelioRoute = {
  path: string;
  Component: React.ElementType;
  routeProps?: RouteProps;
};

// A redirect route
type CrelioRedirectRoute = {
  to: string;
  from: string;
  isRedirect: true | false;
};

// Union type used in route arrays
type CrelioRouteType = CrelioRoute | CrelioRedirectRoute;

// Sidebar item
type CrelioSidebarItem = {
  name: string | JSX.Element;
  icon?: string;
  route?: string;
  onClick?: Function;
  isCustom?: true | false;
  hasPermission?: Function;
  submenuArr?: CrelioSidebarItem[];
  showPartition?: boolean;
  requiredPermissions?: string[];
  hidden?: boolean;
  count?: number | string;
  redirection?: boolean;
};

// Module aggregate
interface Module {
  menu: CrelioSidebarItem[];
  routes: CrelioRouteType[];
  selectedModule: string;
}

Routes support permission-based access control via routeProps, consumed by the AuthRoute component which checks the user's session permissions before rendering.

Route Rendering

The Switch block dynamically renders only the matching module's routes:

<Switch>
  {matchingModule?.routes?.map((route: any) => {
    const {
      to = "", from = "", path = "",
      Component = null, isRedirect = false, routeProps = {},
    } = route;

    const key = `crelio-dashboard-route-${path || to}`;

    if (isRedirect) {
      return <Redirect key={key} exact={true} from={from} to={to} />;
    }

    return (
      <AuthRoute
        key={key}
        exact={true}
        path={path}
        Component={Component}
        routeProps={routeProps}
      />
    );
  })}
  <AuthRoute Component={PageNotFound} />
</Switch>

Only the matchingModule.routes are rendered at any time. Route paths across modules don't need to be globally unique — /dashboard in Finance and /dashboard in Operations won't conflict because only one module's routes are active.

The navigate Utility — v5 ↔ SPA Redirection

Source: src/utils/navigate.ts

The navigate utility is the primary programmatic navigation function used across all modules. It wraps React Router's history.push / history.replace and adds module-aware URL prefixing that makes the same navigation calls work correctly in both v5 single-build mode and SPA mode.

How It Works

import history from "src/utils/history";
import { getReduxState, isLabLogin } from "./helpers";

export type NavigateInput = string | { pathname: string; state?: JsonObject };

export const moduleNames: string[] = [
  "registration", "accession", "operation",
  "finance", "crm", "admin", "inventory",
];

const getUpdatedUrl = (pathname: string, locationPathname: string): string => {
  const sessionData = getReduxState("SESSION");
  const [firstPart] = locationPathname.split("/").filter(Boolean);
  const [path] = pathname.split("/").filter(Boolean);
  const urlBeforeHash = window.location.href.split("#")[0];

  const updatedUrl =
    isLabLogin() &&
    !urlBeforeHash.includes("superAdminUserManagement") &&
    moduleNames.includes(firstPart) &&
    !moduleNames.includes(path) &&
    !urlBeforeHash.includes("inventory")
      ? `/${firstPart}/${pathname}`
      : pathname;

  return removeDoubleSlashes(updatedUrl);
};

export const navigate: Navigate = (urlOrObject, options?) => {
  const method = options?.replace ? history.replace : history.push;
  const updatedUrl = getUpdatedUrl(
    typeof urlOrObject === "string" ? urlOrObject : urlOrObject.pathname,
    history?.location?.pathname
  );
  method(typeof urlOrObject === "string" ? updatedUrl : { pathname: updatedUrl, state: urlOrObject.state });
};

URL Prefixing Logic — getUpdatedUrl

The key function is getUpdatedUrl, which decides whether to prefix the target URL with the current module name:

ConditionResultExample
User is a lab login AND current URL is in a known module AND target path is NOT a known module namePrefix the target with the current moduleIn /operation, navigating to /dashboard/operation/dashboard
Target path IS a known module nameNo prefix — it's a cross-module navigationNavigating to /finance/dashboard/finance/dashboard
URL contains superAdminUserManagementNo prefix — admin context bypassAdmin pages skip module prefixing
URL contains inventoryNo prefix — inventory has its own routing structureInventory module navigation is passed through directly

Why This Matters

In v5 single-build mode, each module runs in its own React app instance with its own <Router>. Routes like /dashboard or /settings are relative to the module and don't need prefixing because the module context is implicit in the HTML entry point.

In SPA mode, all modules share a single <Router>. A component calling navigate("/dashboard") from within the Operations module needs the URL to resolve as /operation/dashboard, not just /dashboard (which would lose the module context). The navigate utility handles this transparently — components don't need to know if they're running in SPA or v5 mode.

Usage

import { navigate } from "src/utils/navigate";

// Simple string navigation
navigate("/testwise-waiting-list/all-tests");

// With state
navigate({ pathname: "/patient-details", state: { patientId: 123 } });

// Replace instead of push (no back button entry)
navigate("/settings", { replace: true });

// Cross-module navigation (explicitly includes module name)
navigate("/finance/dashboard");

SPA Bootstrap — Initialization Sequence

When the SPA loads, the Routes component runs a comprehensive initialization sequence:

useEffect(() => {
  const initialize = async () => {
    // 1. Session + bill settings
    await getAppLicationLevelSessionAndBillSetting();

    // 2. Registration-specific
    await getCustomRegistrationPage();
    await getBillSettingData(dispatch);

    // 3. Operations-specific
    dispatch(getSettingsData("/getOperationSetting/", false));
    dispatch(getReportSettings("/getAllReportSettings/", true));

    // 4. Shared data
    dispatch(getDepartmentList(true));
    await getDepartmentWiseDoctor(dispatch);
    await getDepartmentAndSignDocRel(dispatch);
    await getDoctorList(dispatch);

    // 5. Inventory (conditional)
    inventoryManagement && userInventoryManagement && await fetchInventorySettings();

    // 6. CRM (parallel batch)
    axiosInstance.all([
      "/crm/onboarding/user/permissions/",
      "/crm/store/store-enable-status/",
      "/crm/onboarding/walkthrough/configurations/",
      "/crm/onboarding/walkthrough/",
      "/crm/home-collections/get-crm-settings/",
    ].map(endpoint => axiosInstance.get(endpoint)))
    .then(/* dispatch all CRM state */)
    .finally(() => setLoading(false));
  };
  initialize();
}, []);

This runs once on SPA mount. All subsequent module switches use the data already in Redux.

The SPA includes a cross-module search feature (Cmd+K / Ctrl+K):

useEffect(() => {
  const handleKeyDown = (e: KeyboardEvent): void => {
    if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
      e.preventDefault();
      onSpotlightSearch();
    }
  };
  document.addEventListener("keydown", handleKeyDown);
  return () => document.removeEventListener("keydown", handleKeyDown);
}, []);

The SpotlightSearch component receives the selectedModule to scope search results to the current module context while still enabling cross-module navigation.

Build Pipeline

app-config.js — Entry Point Registry

Source: app-config.js

const allApps = [
  "all",
  // Clubbable modules (SPA-eligible)
  "Admin", "Finance", "inventory", "LabAdmin",
  "Accession", "Operations", "Registration", "crmDashboard", "CrelioDashboard",
  // Standalone modules (separate builds)
  "crm", "DoctorLogin", "ReferralLogin", "MarketingLogin",
  "centerdashboard", "OrganisationLogin",
];

const allBuilds = {
  Admin: "src/modules/Admin/index.tsx",
  Finance: "src/modules/Finance/index.tsx",
  CrelioDashboard: "src/modules/CrelioDashboard/index.tsx",
  // ... each module maps to its entry point
};

Build Path Resolution

BuildOutput DirectoryPublic URL Path
CrelioDashboard./build/CrelioDashboard/static/build_assets/CrelioDashboard/build
All other modules./build/dashboard/static/build_assets/dashboard/build

config-overrides.js — Webpack Customization

Source: config-overrides.js

Key customizations using react-app-rewired + customize-cra:

Multi-entry support:

const build = process?.env?.REACT_APP_BUILD_NAME || "DoctorLogin";
const entries = appConfig.getBuild(build);
const multipleEntry = require("react-app-rewire-multiple-entry")(entries);

esbuild-loader (replaces babel-loader):

rule.oneOf[babelLoaderIndex] = {
  test: /\.(js|jsx|ts|tsx)$/,
  exclude: /node_modules/,
  use: [{
    loader: "esbuild-loader",
    options: { loader: "tsx", target: "es2015" },
  }],
};

Chunk splitting strategy:

splitChunks: {
  chunks: "all",
  maxInitialRequests: 20,
  minSize: 20000,
  cacheGroups: {
    vendor: {
      test: /[\\/]node_modules[\\/]/,
      name(module) {
        const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)?.[1];
        return packageName ? `vendor.${packageName.replace("@", "")}` : "vendor";
      },
      priority: 20,
    },
    commons: {
      test: /[\\/]src[\\/]/,
      name: "commons",
      minChunks: 2,
      priority: 10,
    },
  },
},

This creates per-package vendor chunks (vendor.react.js, vendor.ag-grid-community.js), a commons chunk for shared source code, and a single runtime chunk.

Production Optimizations

PluginPurpose
TerserPluginJavaScript minification
EsbuildPluginAdditional minification + CSS minification
CompressionPluginGzip pre-compression for .js, .css, .html, .svg
MiniCssExtractPluginExtract CSS into separate files with content hashes
ModuleConcatenationPluginScope hoisting for smaller bundles
BundleAnalyzerPluginStatic HTML report of bundle composition
ContextReplacementPluginStrip unused Moment.js locales (keep only English)
NormalModuleReplacementPluginReplace lodash with lodash-es for tree-shaking

Build Output Comparison

AspectSPA BuildAll Build (Legacy)Single Module Build
Commandnps build.CrelioDashboardProdnps build.allProdnps build.RegistrationProd
Entry points1 (CrelioDashboard/index.tsx)15 (all modules)1 (specific module)
HTML files1 (index.html)15 (one per module)1 (index.html)
JS bundlesUnified + code-split per moduleShared chunks + per-module bundlesModule-specific bundle
Output dirbuild/CrelioDashboard/build/dashboard/build/dashboard/
Build time~3-5 min~15-25 min~2-3 min

Asset Deployment — move-assets.sh

After building, assets are copied into the Django project's static directory:

prepare_and_move_assets() {
  cd ../livehealthapp/static
  rm -vrf build_assets/$1
  mkdir -p build_assets/$1/build
  cd ../../livehealth-frontend
  cp -vrf build/$1/* ../livehealthapp/static/build_assets/$1/build
}

CI/CD Pipeline

Bitbucket Pipelines

Source: bitbucket-pipelines.yml

Each module has staging and production build steps:

Build StepBuild Command
registration-prod-buildbuild.RegistrationProd
finance-prod-buildbuild.FinanceProd
crm-prod-buildbuild.crmProd

Jenkins Integration

For the all build (including SPA), Jenkins is triggered via the Bitbucket pipeline:

  1. Checks out the branch.
  2. Runs yarn install and yarn start build.allProd.
  3. Uploads bundled assets to S3 or deploys to EC2.

Post-build steps (Jenkins only):

  1. sentry:production — Uploads source maps to Sentry for error deobfuscation.
  2. delete-sourcemaps — Removes .map files from the build output to prevent client-side source exposure.

Comparison: SPA Module vs. Standalone Module

AspectSPA Module (CrelioDashboard)Standalone Module (e.g., DoctorLogin)
Entry pointCrelioDashboard/index.tsxDoctorLogin/index.tsx
RoutesAll modules combined in one SwitchModule-specific routes only
SidebarShared SideBar with module switcherModule-specific SideBar
InitializationAll settings fetched onceModule-specific settings only
Module switchingIn-memory route changeFull page reload
SessionShared staff sessionRole-specific session
Build outputcrelio-dashboard.htmldoctor-login.html

Key Frontend Locations

FilePurpose
CrelioDashboard/index.tsxSPA entry point — renders Main with unified Routes
RoutesAndMenus/routes.tsxSPA-level routing: module resolution, initialization, sidebar binding
routeTypes.tsTypeScript type definitions for routes, sidebar items, and modules
Sidebar/sidebar.tsxUnified sidebar with module-aware menu rendering
SwitchModuleDropdown/Module switcher dropdown component
src/utils/navigate.tsModule-aware navigation utility — handles URL prefixing for v5 ↔ SPA compatibility
app-config.jsModule registry: maps build names to entry points
config-overrides.jsWebpack customization: esbuild-loader, chunk splitting
package-scripts.jsnps build commands for local dev and production builds
bitbucket-pipelines.ymlCI/CD pipeline: per-module build steps
move-assets.shAsset deployment: copies build output into Django static directory
src/setupProxy.jsDev server proxy: routes /api-v3/ to Py3 backend

On this page