Skip to main content

Frontend Architecture

Repository: equa-web | Stack: React 16.11, TypeScript 3.7, Redux 4, Webpack 4 Last updated: 2026-02-28 (Spec 050 — SPA hydration performance optimization)

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 ReactDOM.render().

Module Inventory

The frontend is organized into 24 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
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 connected-react-router 6.3.2
  • 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 44 JS chunks: 4 named entrypoint chunks (react-vendor, styled, vendor, main) 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-redux7.2.6
Async actionsredux-thunk2.3.0
Side effectsredux-loop4.5.4
Router syncconnected-react-router6.3.2
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()

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 4.2equa-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 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:
Cache GroupNamePriorityContent
reactreact-vendor20react, react-dom, react-router, react-redux, redux
styledstyled15styled-components
vendorvendor10All other initial node_modules
Additional settings: maxInitialRequests: 10, minSize: 20000. Lazy-loaded module chunks are NOT merged into the vendor group (chunks: 'initial'), ensuring heavy libraries like pixi.js-legacy, ethers, and react-pdf stay in their own async chunks.

Performance Budgets

BudgetValueMode
Max entrypoint size500 KBwarning
Max asset size300 KBwarning
These budgets emit build warnings for oversized chunks. The current entrypoint (react-vendor + styled + vendor + main) is 1.3 MB gzipped, above the 500 KB target. Further reduction requires upgrading to Webpack 5 (better tree shaking) and React 18 (smaller runtime).

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
JestUnit tests
PlaywrightE2E tests (e2e/ directory)

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