Skip to main content

Frontend Architecture

Repository: equa-web | Stack: React 18.2, TypeScript 5.3, Redux 4, Webpack 4 Last updated: 2026-04-11 (Phase 1 React 18 upgrade, ecosystem libraries, bundle optimization, API client network-error handling, dashboard empty state, gateway token sync)

Entry Point

Source: equa-web/src/index.tsx and equa-web/lab/main-prod.ts The app initializes polyfills (core-js/stable + regenerator-runtime/runtime), creates a Redux store via newStore(), creates browser history with createBrowserHistory(), wraps in a Redux Provider, and renders to #root via createRoot() from react-dom/client (upgraded from ReactDOM.render() in PR #506).

Module Inventory

The frontend is organized into feature modules under equa-web/src/modules/:
ModuleDirectoryPurpose
adminmodules/admin/Admin dashboard, user management, site stats
agreementsmodules/agreements/Governing documents management
authmodules/auth/Login, registration, password reset, email verification
captablemodules/captable/Cap table management, shareholdings, certificates
convertiblesmodules/convertibles/Convertible instruments and notes
documentsmodules/documents/Data room and document management
equanautmodules/equanaut/AI agent integration, action executor, onboarding
esopmodules/esop/Employee stock option plans, pools, vesting schedules
google-drivemodules/google-drive/Google Drive sync integration
guestmodules/guest/Guest user pages (portfolio, dashboard, referrals)
hh-financemodules/hh-finance/Finance dashboard, transactions, metrics
landingmodules/landing/Landing page, auth modal
organizationmodules/organization/Organization management, securities, legends
organization-dashboardmodules/organization-dashboard/Organization dashboard, capital summary, empty-state onboarding guide
actionsmodules/actions/Organization actions, feature requests
paymentsmodules/payments/Billing, payment profiles, checkout, subscriptions
profilemodules/profile/User profile, portfolio, wallet, account settings
referralsmodules/referrals/Referral system, EquaCash transfers
reportsmodules/reports/Reports (holder, pools, holdings)
rolesmodules/roles/Role management, permissions
subscriptionsmodules/subscriptions/Subscription management
team-membersmodules/team-members/Team member management, invitations
user-dashboardmodules/user-dashboard/User dashboard
welcomemodules/welcome/Onboarding flow, profile building, PIN setup

Routing and Code Splitting

Source: equa-web/src/app/routes/routes.ts, equa-web/src/app/routes/lazy.tsx, equa-web/src/app/components/routes/routes.tsx
  • Router: React Router v5 (react-router-dom 5.1.2) with redux-first-history (replaced connected-react-router in PR #465)
  • Route groups: profileRoutes, guestRoutes, guestOrganizationsRoutes, organizationListRoutes, organizationRoutes, routes
  • Protection: Routes flagged as protected redirect unauthenticated users
  • HOCs: withNavigation, withTracker, withService, withHeader, withOrganizationHeader, withProfileHeader
  • Path constants: equa-web/src/logic/paths.ts

Lazy Loading (Spec 050)

All feature module page components are lazy-loaded via React.lazy() + <Suspense>. The lazyPage() utility in src/app/routes/lazy.tsx wraps each dynamic import with a <Suspense> fallback using the existing <Loading /> spinner component.
const CaptablePage = lazyPage(() =>
  import('@modules/captable').then(m => ({ default: m.CaptablePage }))
)
What stays in the main bundle (static imports):
  • Auth module (@modules/auth/pages) — needed for login page to render immediately
  • Shell components: headers, navigation, loading, error boundary, permissions HOCs
  • Redux reducers (all 10 registered at store initialization per clarification C5)
What is lazy-loaded (137 dynamic imports across 25+ module groups):
  • All other page components: captable, ESOP, organization, convertibles, payments, profile, guest, admin, referrals, reports, roles, documents, welcome, team-members, hh-finance, google-drive, equabot-settings, agreements, landing, marketing, messaging, user-dashboard, organization-dashboard
The production build produces 48+ JS chunks: 4 named entrypoint chunks (react-vendor, styled, vendor, main) plus 4 async vendor chunks (crypto-vendor, pixi-vendor, form-vendor, pdf-vendor) plus ~40 lazy module chunks.

Chunk Error Boundary

Source: equa-web/src/app/components/routes/chunk-error-boundary.tsx A ChunkErrorBoundary wraps the route <Switch> in routes.tsx. If a lazy chunk fails to load (network error, deployment mismatch), it catches the error via getDerivedStateFromError and renders a “Failed to load this page” message with a Retry button instead of a white screen.

Adding New Routes

When adding a new route or module page:
  1. Always use lazyPage() for the component import — never add a static import from a module barrel
  2. Follow the pattern: const NewPage = lazyPage(() => import('@modules/new-module').then(m => ({ default: m.NewPage })))
  3. The only exception is @modules/auth/pages which must stay static for the login critical path

State Management

Source: equa-web/src/logic/store.ts, equa-web/src/logic/reducers/root-reducer.ts
ComponentLibraryVersion
StoreRedux4.0.1
React bindingsreact-redux^8.1.0
Async actionsredux-thunk2.3.0
Side effectsredux-loop4.5.4
Router syncredux-first-history^5.x
DevToolsRedux DevTools Extension

Reducers

ReducerPurpose
userReducerCurrent user state, auth status
organizationReducerActive organization data
accessReducerPermissions and access control
toastsReducerToast notification queue
myReferralReducerReferral program state
capTableReducerCap table data cache
checklistReducerOnboarding checklist progress
serviceReducerAPI service configuration

API Communication

Source: equa-web/src/service/lib/http-client.ts, equa-web/src/service/services/web-client.ts
  • HTTP client: Native fetch() for standard requests, axios 0.21.1 for multipart/form-data
  • Base URL: Configured via API_URL (default: /api/v1)
  • Credentials: credentials: 'include' (session cookies)
  • Error handling: HttpError class with auto-logout on 401
  • Methods: get(), post(), patch(), put(), delete(), postMultipart(), postFiles()

Network Error Handling (PR #514)

baseRequest() in http-client.ts wraps every fetch() call in a try-catch. When the backend is unreachable — DNS failure, CORS block, connection refused, offline — fetch() throws a TypeError. Rather than letting that throw propagate as an unhandled rejection (which was the root cause of the infinite splash screen bug, issue #482), the client converts it into a structured error:
return new HttpError(ResponseCode.other, 'networkError', /* ... */)
This lets the normal redux-loop failure path dispatch, the getCurrentUser callback fires, initialLoad flips to true, and the app renders the login page instead of hanging on the Equa logo spinner forever. Callers do not need special handling — the network-error response follows the same HttpError contract as 4xx/5xx responses. Anywhere the code already checks isHttpError(result), the network-error case is handled automatically. Source: equa-web/src/service/lib/http-client.ts (PR #514, 2026-04-02).

Service modules

Located in equa-web/src/service/services/:
  • actions, billing, captable, google-drive, organizations, payments, profile, roles, wallet

Component Library

Source: equa-web/package.json (dependency: equa-patternlib from GitHub) The pattern library provides 26+ shared UI components: Avatar, Badge, Button, Card, Checkbox, Chip, DatePicker, Dropdown, FileUpload, Input, Menu, Modal, Pagination, Progress, Radio, Rating, Sidebar, Skeleton, Slider, Stepper, Switch, Table, Tabs, Toast, Toggle, Tooltip Components are re-exported via equa-web/src/shared/components/.

Styling

ApproachLibrarySource
Primarystyled-components ^6.1.0equa-web/src/styles/styled.ts
Global stylescreateGlobalStyleequa-web/src/styles/global.ts
ThemeThemeProviderequa-web/src/styles/theme.ts
Utilitiespolished 3.4.1Color manipulation
SCSSsass-loader (webpack)Legacy components
Theme switching is supported and persisted via cookies.

Font Loading

Six NunitoSans variants are declared via @font-face in src/styles/global.ts. All declarations include font-display: swap to prevent Flash of Invisible Text (FOIT). When adding new @font-face rules, always include font-display: swap.

styled-components Build Plugin

The babel-plugin-styled-components (^2.1.4) is active in the webpack babel-loader config. In production builds it strips displayName and enables pure annotation for dead code elimination. In development it preserves displayName for debugging.

Webpack Configuration

Source: equa-web/webpack.config.js
SettingValue
Entrylab/main-prod.ts
Outputdist/ with content hashes ([name]-[hash].js)
Dev server port8080
API proxy/api -> http://localhost:3000
Equanaut proxy/equanaut-api -> http://localhost:19792
Production minifierTerserPlugin
Bundle analyzerANALYZE_BUNDLE=1 env var triggers webpack-bundle-analyzer
Babel pluginsbabel-plugin-styled-components (production: displayName: false, pure: true)

splitChunks (Production Only)

The production build uses cacheGroups to separate vendor code into named chunks for optimal caching: Initial chunks (loaded on every page):
Cache GroupNamePriorityContent
reactreact-vendor20react, react-dom, react-router, react-redux, redux
styledstyled15styled-components
vendorvendor10All other initial node_modules
Async chunks (loaded on-demand when navigating to specific routes):
Cache GroupNamePriorityContentLoaded When
cryptocrypto-vendor25ethers, web3, crypto libs (18 KB)Wallet/MetaMask pages
pixipixi-vendor25pixi.js-legacy (629 KB)Chart/visualization pages
formform-vendor15react-select, react-datepickerPages with form components
pdfpdf-vendor15react-pdf, pdfjs-distDocument viewer pages
Additional settings: maxInitialRequests: 10, minSize: 20000. The async vendor chunks were added in PR #510 to reduce the initial page load by ~2.5 MB — pixi.js, crypto libs, and the login background image (compressed 95% from 1.93 MB to 93 KB) are no longer loaded on first visit.

Performance Budgets

BudgetValueMode
Max entrypoint size500 KBwarning
Max asset size300 KBwarning
These budgets emit build warnings for oversized chunks. The entrypoint size was significantly reduced in PR #510 by moving heavy vendor libraries to async chunks (~2.5 MB reduction). Further reduction requires upgrading to Webpack 5 (better tree shaking) or migrating to Vite (Phase 2).

Polyfill Strategy

The entry point imports core-js/stable and regenerator-runtime/runtime (replacing the deprecated @babel/polyfill). The @babel/preset-env handles transpilation targeting browsers specified in the webpack config.

Path Aliases

AliasMaps To
@srcsrc/
@logicsrc/logic
@configlocal.json
@stylessrc/shared/styles
@modulessrc/modules
@componentssrc/shared/components
@sharedsrc/shared
@helperssrc/shared/helpers
@imagesrc/assets/image

Key Utilities

UtilityFilePurpose
Type helperssrc/shared/helpers/util.tsUuid, Hash, Money, formatting
Validatorssrc/shared/helpers/field-validators.tsForm field validation
Data cachesrc/shared/helpers/data-cache.tsClient-side caching
Shareholdingssrc/shared/helpers/shareholdings.tsShareholding calculations
MS Graphsrc/shared/helpers/ms/msGraph.tsMicrosoft integration
Constantssrc/shared/helpers/constants.tsxApp-wide constants

Coding Conventions

Lodash Imports

Use per-function imports, never the full library:
import range from 'lodash/range'
import isEqual from 'lodash/isEqual'
Never use import _ from 'lodash' — this pulls the entire 72 KB library into the bundle even if only one function is used. If a file doesn’t actually call any lodash function, remove the import entirely.

Route Imports

All page component imports in routes.ts must use lazyPage() dynamic imports. The only exception is @modules/auth/pages (static for the login critical path). See Routing and Code Splitting above.

Dependency Hygiene

Before adding a new dependency, check if it will end up in the initial entrypoint bundle or a lazy chunk. Heavy libraries (>100 KB) should only be imported from lazy-loaded modules. The following unused packages were removed during Spec 050 and must not be re-added: moment (use date-fns instead), recharts, react-hot-loader, @hot-loader/react-dom.

Testing

FrameworkPurpose
Jest 29 + React Testing Library 14Unit tests (migrated from Jest 26 + Enzyme in PR #465)
PlaywrightE2E tests (e2e/ directory)
Visual regressionScreenshot comparison in CI (PR #509)

Performance Testing

Source: Comet-Bridge/scripts/perf-audit.mjs An automated performance audit script runs via Comet-Bridge Playwright against the equa-web dev server or production build. It navigates all major route groups, captures performance.timing metrics (TTI, DCL, load time), checks for chunk load errors, and produces a JSON report with screenshots.
cd Comet-Bridge
node scripts/perf-audit.mjs --url http://localhost:8080
node scripts/perf-audit.mjs --url http://localhost:8090 --throttle  # fast 3G
Output: ~/.claude/comet-browser/output/audit/s050-perf/perf-report.json