Frontend Architecture
The frontend is a React 19 single-page application built with Vite 8, styled with Tailwind CSS v4, and internationalized with react-intl. It is served as static files by nginx, which also reverse-proxies API requests to the backend.
Technology Stack
Section titled “Technology Stack”| Technology | Version | Purpose |
|---|---|---|
| React | 19.3 | UI framework |
| Vite | 8.3 | Build tool and dev server |
| Tailwind CSS | 4.3 | Utility-first styling |
| react-intl | 12.1 | Internationalization (fr/en/es/de/it) |
| React Router | 8.5 | Client-side routing |
| TanStack Query | 5.104 | Server state management |
| Recharts | 3.10 | Charting library |
| dnd-kit | 6.3 / 10.0 | Drag-and-drop (sortable grids, lists) |
| react-hook-form + Zod | 7.91 / 4.6 | Form handling and validation |
| Base UI + Radix UI | 1.6 / 1.5 | Headless UI primitives |
| lucide-react | 1.61 | Icon library |
| date-fns | 4.4 | Date utilities |
| react-grid-layout | 2.3 | Dashboard card grid (12 columns) |
36 pages in src/pages/. Eagerly loaded pages are imported directly in App.tsx; all others are lazy-loaded via React.lazy + Suspense.
| Page | File | Loading | Route | Description |
|---|---|---|---|---|
| DashboardPage | DashboardPage.tsx |
Lazy | /dashboard |
Net-worth overview, charts, savings goals |
| AccountsPage | AccountsPage.tsx |
Lazy | /accounts |
Account list, CRUD, drag-and-drop reorder |
| SnapshotsPage | SnapshotsPage.tsx |
Lazy | /snapshots |
Snapshot list grouped by date |
| SnapshotEntryPage | SnapshotEntryPage.tsx |
Lazy | /snapshots/new |
Grid-based snapshot entry form |
| ChartsPage | ChartsPage.tsx |
Lazy | /graphic |
Full chart gallery (net worth, allocation, heatmap, waterfall); queries lazy-loaded per active tab via enabled option |
| ImportPage | ImportPage.tsx |
Lazy | /import |
File upload, preview, mapping, commit |
| ExportPage | ExportPage.tsx |
Lazy | /export |
Export to CSV/JSON/ODS/XLSX/backup |
| HistoryPage | HistoryPage.tsx |
Lazy | /history |
Snapshot history and restore |
| SettingsPage | SettingsPage.tsx |
Lazy | /settings |
User profile, locale, password, API tokens, webhooks |
| SystemPage | SystemPage.tsx |
Lazy | /system |
System info, exchange rates |
| AuditLogPage | AuditLogPage.tsx |
Lazy | /audit |
Paginated audit log viewer |
| BudgetPage | BudgetPage.tsx |
Lazy | /transactions |
Transactions and budgets, monthly summary |
| BudgetChartsPage | BudgetChartsPage.tsx |
Lazy | /transactions/charts |
Budget-vs-actual and goal-projection charts |
| CategorizationRulesPage | CategorizationRulesPage.tsx |
Lazy | /transactions/rules |
Rule-based auto-categorization CRUD |
| PlannedPage | PlannedPage.tsx |
Lazy | /planned |
Planned/recurring transaction CRUD, due/overdue status, reminders (merged former Bills + Recurring Transactions); Planned / Subscriptions tabs (?view=subscriptions): yearly cost, price changes and detected subscriptions, all figures from the server |
| TagsPage | TagsPage.tsx |
Lazy | /tags |
Tag CRUD, assignment |
| PayeesPage | PayeesPage.tsx |
Lazy | /payees |
Payee list with server-side search and sort, create, rename, delete unused |
| CategoriesPage | CategoriesPage.tsx |
Lazy | /categories |
Category/subcategory CRUD, reorder |
| InvestmentsPage | InvestmentsPage.tsx |
Lazy | /invest |
Holdings, dividends, portfolio summary |
| InstitutionsPage | InstitutionsPage.tsx |
Lazy | /institutions |
Institution CRUD, aggregated balances, archive/unarchive with cascade, soft-delete with cascade to accounts/snapshots, trash dialog with restore and permanent delete, empty trash; card and editable table views with sortable columns (name, solde) |
| ExchangeRatesPage | ExchangeRatesPage.tsx |
Lazy | /exchange-rates |
Exchange rate CRUD (create, populate-on-edit update, delete with confirmation), filterable/sortable paginated table, bulk refresh from ECB, single-pair historical fetch, per-pair rate-history chart |
| SavingsGoalsPage | SavingsGoalsPage.tsx |
Lazy | /savings-goals |
Savings goal CRUD, linked account progress |
| AnalysePage | AnalysePage.tsx |
Lazy | /analytic |
Liquidity and concentration analysis |
| ReportsPage | ReportsPage.tsx |
Lazy | /reports |
Financial independence, cash flow, report builder and retirement simulation tabs (?section=), saved reports |
| SummaryPage | SummaryPage.tsx |
Lazy | /summary |
Cross-account financial summary |
| UserManagementPage | UserManagementPage.tsx |
Lazy | /users |
Admin-only user list, lock/unlock, role changes, invites |
| ProfilePage | ProfilePage.tsx |
Lazy | /profile |
User profile summary |
| LoginPage | LoginPage.tsx |
Eager | /login |
Local login + OIDC redirect |
| RegisterPage | RegisterPage.tsx |
Eager | /register |
New user registration (if enabled) |
| ForgotPasswordPage | ForgotPasswordPage.tsx |
Eager | /forgot-password |
Local password reset request |
| ResetPasswordPage | ResetPasswordPage.tsx |
Eager | /reset-password |
Local password reset confirmation |
| ConfirmEmailPage | ConfirmEmailPage.tsx |
Eager | /confirm-email |
Email-change confirmation via token |
| AcceptInvitePage | AcceptInvitePage.tsx |
Eager | /accept-invite |
Admin-issued invite acceptance/registration |
| DuplicatesPage | DuplicatesPage.tsx |
Lazy | /transactions/duplicates |
Probable duplicate transactions: review, merge in bulk or dismiss |
| EnvelopesPage | EnvelopesPage.tsx |
Lazy | /envelopes |
Envelopes: sinking funds, contributions, mark paid, free-to-use figure, payday plan |
| NotFoundPage | NotFoundPage.tsx |
Eager | * |
Unknown route |
84 custom hooks in src/hooks/ (81 .ts + useAuth.tsx, useRuleFromEdit.tsx and useSimilarOffer.tsx), most wrapping TanStack Query operations.
| Hook | File | Purpose |
|---|---|---|
createCrudHooks |
createCrudHooks.ts |
Generic factory that builds a set of CRUD query/mutation hooks for a resource; the base most entity-specific hooks below are built on |
useAccounts |
useAccounts.ts |
Account CRUD queries and mutations; includes useBulkDeleteAccounts and useBulkArchiveAccounts for parallel multi-account operations with single cache invalidation, and useEmptyTrash for permanently deleting all soft-deleted accounts |
useAccountSort |
useAccountSort.ts |
Sortable-column state for the accounts table |
useAccountsViewMode |
useAccountsViewMode.ts |
Accounts view mode (cards/table toggle, archive filter) extracted from AccountsPage |
useAdminUsers |
useAdminUsers.ts |
Admin user list, lock/unlock, role-change mutations |
useAuditLogs |
useAuditLogs.ts |
Paginated audit log query |
useAuth |
useAuth.tsx |
Auth context/state, login, logout, register (a hook file using JSX for its provider) |
useAuthConfig |
useAuthConfig.ts |
Auth configuration (OIDC/registration enabled); useDocsUrl returns the documentation site URL (docs_url) when it is an http(s) URL, otherwise null |
useBaseCurrency |
useBaseCurrency.ts |
Returns authenticated user’s base currency (default 'EUR') — use instead of hardcoding 'EUR' |
usePayees |
usePayees.ts |
Payee list (search, sort, first 250), create, rename, delete; usePayeeSearch (server search, 50 most-used matches) and usePayeeLookup (names for known ids) back PayeeCombobox |
usePlannedTransactions |
usePlannedTransactions.ts |
Planned transaction CRUD queries and mutations |
useBudget |
useBudget.ts |
Transaction and budget queries/mutations; useTransactionsQuery supports enabled option |
useCategories |
useCategories.ts |
Category/subcategory queries and mutations |
useCategorizationRules |
useCategorizationRules.ts |
Rule-based auto-categorization CRUD |
useChartAnnotations |
useChartAnnotations.ts |
Chart annotation CRUD |
useCharts |
useCharts.ts |
Chart data queries (net worth, trends, allocation); supports enabled option for lazy loading per active tab |
useCsvCategoryImport |
useCsvCategoryImport.ts |
Bulk category import from CSV mutation |
useCsvTransactionImport |
useCsvTransactionImport.ts |
Generic bank CSV import: parse (multipart, with the account and the mapping or preset) and commit mutations |
useDashboards |
useDashboards.ts |
The user’s dashboards: the list query, the first-load upload of the card order the browser used to keep in localStorage (sent once to POST /api/v1/dashboards/initialize, forgotten only after it succeeds), and useSaveDashboardLayout, which saves a layout with an optimistic cache update, rollback and a toast on failure |
useDebouncedValue |
useDebouncedValue.ts |
Generic debounced-value hook for search/filter inputs |
useExchangeRates |
useExchangeRates.ts |
Exchange rate CRUD (useExchangeRatesQuery filterable/sortable paginated list, useCreateExchangeRate/useUpdateExchangeRate/useDeleteExchangeRate), useRefreshFxRates (bulk auto-fetch), useFetchHistoricalFxRate (single-pair fetch), useExchangeRateHistoryQuery (unpaginated per-pair history for the rate chart) |
useExport |
useExport.ts |
Export download triggers |
useImport |
useImport.ts |
File parse and commit mutations |
useInstitutions |
useInstitutions.ts |
Institution CRUD queries and mutations; includes useDeletedInstitutionsQuery for trash listing, useArchiveInstitution/useUnarchiveInstitution for archive with cascade to accounts, useRestoreInstitution for restoring soft-deleted institutions with cascade to accounts/snapshots, usePermanentDeleteInstitution for irreversible deletion, and useEmptyInstitutionsTrash for permanently deleting all soft-deleted institutions; all destructive mutations invalidate accounts, snapshots, and charts query keys on success |
useInstitutionsViewMode |
useInstitutionsViewMode.ts |
Institutions view mode (cards/table toggle, archive filter) persisted to localStorage; mirrors useAccountsViewMode |
useInvestments |
useInvestments.ts |
Holdings and dividends queries/mutations; useDeleteHolding accepts onSuccess/onError callbacks |
useKeyboardShortcuts |
useKeyboardShortcuts.ts |
Global keyboard shortcut handler |
useLogout |
useLogout.ts |
Logout mutation, cache/localStorage cleanup |
useMarketData |
useMarketData.ts |
Ticker lookup/price queries backing investment holdings |
useMediaQuery |
useMediaQuery.ts |
useMediaQuery(query): whether a media query matches, through window.matchMedia and useSyncExternalStore; false without matchMedia, so jsdom page tests take the desktop branch. useIsPhone() is below 640 px |
useMutationToast |
useMutationToast.ts |
Shared toast-notification helper for mutation success/error callbacks |
useNetWorthThresholds |
useNetWorthThresholds.ts |
Net-worth threshold CRUD |
useOfxImport |
useOfxImport.ts |
OFX file parse and commit mutations |
useMergeTransactions |
useMergeTransactions.ts |
Merge two duplicate transactions (the kept one fills its gaps from the other, which goes to the trash) |
usePrivacyMode |
usePrivacyMode.ts |
Privacy flag read from the lib/privacy.ts store with useSyncExternalStore, plus its setters |
useRecurringSnapshots |
useRecurringSnapshots.ts |
Recurring snapshot CRUD |
useRuleFromEdit |
useRuleFromEdit.tsx |
Offers, for users who switched it on in Settings, to turn a categorization into a rule: a toast whose action opens the rule editor in a dialog, prefilled; it never creates a rule itself |
useSavedFilters |
useSavedFilters.ts |
Saved filter list query and create, rename and delete mutations |
useCashFlow |
useCashFlow.ts |
Cash flow graph for a period; keeps the previous period on screen while the next one loads |
useFireProjection |
useFireProjection.ts |
Financial independence projection (FIRE_QUERY_KEY, invalidated by a settings save) |
useReportSettings |
useReportSettings.ts |
Saved report settings query and the PATCH mutation, which invalidates the ['reports'] queries |
useRetirementSimulation |
useRetirementSimulation.ts |
Mutation that saves the simulation settings, then runs GET /api/v1/reports/simulation from the saved values |
useRunReport |
useRunReport.ts |
Mutation that runs a report definition (POST /api/v1/reports/run) |
useSavedReports |
useSavedReports.ts |
Saved report list query and create, update and delete mutations |
useSavingsGoals |
useSavingsGoals.ts |
Savings goal CRUD, account linking; supports enabled option |
useSelection |
useSelection.ts |
Generic multi-select state (selectedIds, toggle, selectAll) shared across list pages |
useSidebarCollapse |
useSidebarCollapse.ts |
Sidebar collapsed/expanded state persisted to localStorage |
useSkin |
useSkin.ts |
Active skin (Fintech/Azur/Noir/Swiss/Aurora) via data-skin on <html>, persistence via localStorage, and sync with the user profile (user.theme) |
useCustomTheme |
useCustomTheme.ts |
Applies the custom properties of user.custom_css with style.setProperty on <html> (inline, so they outrank the skin and mode blocks), removes them when the value changes or on sign-out, and skips them for the whole page load when the address carries ?safe-theme=1. useCustomCssRules, in the same file, reads the allowed property names and the maximum length from GET /api/v1/auth/custom-css/rules |
useSnapshots |
useSnapshots.ts |
Snapshot CRUD, counts, batch operations |
useSnapshotsViewMode |
useSnapshotsViewMode.ts |
Snapshots view mode persisted to localStorage |
useSplits |
useSplits.ts |
Replace and remove the category lines of a split transaction |
useSubcategories |
useSubcategories.ts |
Subcategory CRUD, reorder |
useTags |
useTags.ts |
Tag CRUD, assignment to transactions |
useTourSeen |
useTourSeen.ts |
useMarkTourSeen: records on the server that the guided tour was offered, retried once, never toasts |
useTransactionSelection |
useTransactionSelection.ts |
Multi-select state for the transactions table |
useWebhooks |
useWebhooks.ts |
Webhook CRUD |
useAlerts / useAlertsFeed / useAlertRules / useNotificationPreferences |
useAlerts.ts, useAlertsFeed.ts, useAlertRules.ts, useNotificationPreferences.ts |
Notification centre feed, unread count and read/dismiss actions, the three alert rules and the channel per notification type |
useEnvelopes |
useEnvelopes.ts |
Envelope CRUD, contributions, pay (sends an Idempotency-Key), payday plan |
useDuplicates |
useDuplicates.ts |
Probable duplicates and dismissals |
useReconcile |
useReconcile.ts |
Reconcile preview, commit and undo |
useTransfers |
useTransfers.ts |
Create, link, unlink and suggest transfers |
useSessions |
useSessions.ts |
Signed-in sessions: list, revoke, revoke others |
useRecoveryCodesStatus |
useRecoveryCodesStatus.ts |
Remaining two-factor recovery codes |
useSpendable / useSavingsRate / useSubscriptions / usePlannedSuggestions |
useSpendable.ts, useSavingsRate.ts, useSubscriptions.ts, usePlannedSuggestions.ts |
Read-only figures from the server |
useTabParam / useOffsetUrlState / useServerSort |
useTabParam.ts, useOffsetUrlState.ts, useServerSort.ts |
URL-synchronised tab, paging offset and server-side sort |
useGlobalSearch |
useGlobalSearch.ts |
Command palette server search |
useSimilarOffer |
useSimilarOffer.tsx |
The “Apply to similar” dialog host and the offer shown after a transaction gets a category |
useTrashDialogState / useUndoToast / useFocusableWhenScrollable |
useTrashDialogState.ts, useUndoToast.ts, useFocusableWhenScrollable.ts |
Trash dialog state, undo toast, keyboard focus for scrollable regions |
Component Subdirectories
Section titled “Component Subdirectories”24 subdirectories (23 feature directories plus __tests__/) under src/components/ (plus 4 root-level files):
| Directory | Files | Key Components |
|---|---|---|
accounts/ |
19 | AccountCard, AccountForm/AccountFormDialog, AccountRow, AccountTable (with balance sorting), AccountTableCell, AccountTypeBadge, AccountTypeSelect, ColorPicker, CurrencySelect, EmptyState, InstitutionSelect, LiquidityHorizonSelect, AccountLabel, BudgetToggleButton, CloseAccountDialog, LoanPayoffDialog, LoanPayoffLine |
admin/ |
1 | AdminAuditLogTab |
auth/ |
1 | RouteGuard — protects routes, redirects to /login if unauthenticated |
brand/ |
1 | BrandMark |
budget/ |
31 | TransactionsTable, TransactionForm/TransactionFormDialog, TransactionRow, TransactionFilterPanel, AccountCell, BudgetsSection, BulkEditTransactionsDialog, ImportCsvCategoryDialog, ImportCsvTransactionsDialog (column mapping and live preview for a bank CSV), ImportOfxDialog, MergeTagsDialog, MergePayeesDialog (the payees to merge, the one kept chosen explicitly, with a live count of the transactions that move), TagMultiCombobox, PayeeCombobox (the payee picker: a debounced server search, an ids lookup for the selected one, and a create option), SplitDialog (the category lines of a split, with a live remainder), MergeDialog (two duplicates side by side, the kept one chosen explicitly), SavedFiltersPopover (save, apply, rename and delete saved filters), CategoryMultiSelect (the category and subcategory filter, shared with the report builder) |
categories/ |
9 | CategoryTree, CategoryTreeNode, CategoryCard, CategoryCombobox, CategorySelect, SubcategoryCard, MoveSubcategoryDialog, categoryTreeArborist.ts |
categorizationRules/ |
5 | ConditionGroupEditor, PayeeConditionCombobox (the Payee condition’s value: one payee for is, name chips for is one of, ids stored and names always shown), RuleEditor (the rule form, shared by the rules page and a dialog), RuleEditorDialog |
charts/ |
64 | NetWorthChart, AccountTrendsChart, StackedAreaChart, DeltaHeatmap, WaterfallChart, AllocationTreemap, AssetsLiabilitiesDonut/AssetsLiabilitiesGauge, CurrencyExposureDonut, SavingsGoalsProgress, SavingsRateTrend, ChartCard, ChartTooltip, ChartEmptyState, DateRangeSelector, BudgetVsActualChart, GoalProjectionTimeline, DividendCalendarChart/DividendGrowthChart, DrawdownChart, ConcentrationChart, ConsistencyGauge, LiquidityChart, SnapshotHeatmap, SpendingHeatmap, CategorySpendingDonut, AllocationByInstitutionDonut/AllocationByTypeDonut/AllocationEvolutionBarChart, per-tab wrappers (ChartTabContent, AllocationTabContent, AnalysisTabContent, EvolutionTabContent, TrackingTabContent), ChartAnnotationForm, chart-utils.ts, useChartHeight.ts, ForecastChart, ForecastWhatIf, ForecastTabContent, YoYComparisonChart, MonthlyIncomeExpenseChart, NominalVsAdjustedChart, ChartAnnotationsDialog |
dashboard/ |
11 | DashboardCards (barrel), DashboardGrid (the cards on a 12-column react-grid-layout grid at 640px and up, a stacked list below; reports a move or resize on release), DashboardCardMenu (a card’s options menu in Customize mode), DashboardTabs (one tab per dashboard and the Manage dashboards button), ManageDashboardsDialog (the Manage dashboards dialog), TidyUpDialog (the Tidy up preview: the summary sentences, the numbered before and after diagrams, Apply and Cancel, and the already-compact state), LayoutDiagram (the SVG block diagram of one layout, drawn from theme tokens and hidden from assistive technology because the captions carry the numbers), dashboard-utils.tsx (renderDashboardCard, getAvailableCardIds, computeDashboardData), card-registry.tsx (each card’s component and preview, keyed by the catalog), card-example-data.ts (the example data of the previews), library/ subdirectory: CardLibraryDialog, CardLibraryTile and CardThumbnail (the Card library gallery); the catalog (ids, names, groups, sizes, default set) lives in lib/dashboard-catalog.ts, the layout rules in lib/dashboard-layout.ts, including packLayout, the pure packer behind Tidy up (first-fit placement over the cards in reading order and over the reading order cut into blocks sorted by height, width and area; the shortest result wins, then the one closest to the reading order; when that is taller than the cards’ area needs, an exact depth-first search tries each shorter height within a fixed node budget, and the default cards are laid out with its result; offered only when strictly shorter than what the grid draws, so never taller; a heuristic, not guaranteed minimal; it reports rows, empty cells and moved cards), and the library’s search and preview-size helpers in lib/dashboard-library.ts + cards/ subdirectory: NetWorthCards, BalanceCards, AccountCards, RecordCards, GrowthCards, GoalCards, StructureCards, MoneyMonthCards, PlanCards, InvestmentCards, types.ts, index.ts, SetupChecklist; visuals/ subdirectory: VisualBody, SegmentBar, ProgressBar, Ring, Donut, DonutLegend (a donut beside its legend), WaterfallRows, MiniBars, SignedBars (signed columns about a zero axis), DotRow (weekly dots with the current one ringed) (the SVG and CSS primitives of the visual cards and of the charts on the standard cards; Ring takes ticks and a band, ProgressBar a thin option); lib/dashboard-visuals.ts holds the pure models and rules of those cards and hooks/useVisualCardData.ts fetches their data only for placed cards or while the library is open |
envelopes/ |
4 | EnvelopeForm, PaydayPlanCard, ScheduleFormDialog, FundingScheduleSelect |
exchange-rates/ |
7 | RatesTable, RateFormDialog, RateFiltersBar, RateHistoryChart, CoverageAuditPanel |
import/ |
8 | FileDropZone, PreviewTable, BackupPreviewTable, ImportSummary, AccountMappingSummary, ImportErrorAlert, RulesTimedOutNotice |
institutions/ |
4 | InstitutionsGridView/InstitutionsGridRow — grid view (not a table) with inline name/color editing, aggregated balance, status badge, actions menu; InstitutionCard, InstitutionFormDialog |
investments/ |
11 | HoldingsTable, HoldingForm/HoldingFormDialog, HoldingGroupRow, HoldingLotRow, HoldingsFilterPanel, PortfolioSummaryCards, AccountSelect, DividendsDialog/DividendForm/DividendRow |
layout/ |
21 | AppLayout, TopNavbar, Sidebar, BottomNav, ModeToggle, SkinToggle, LanguageToggle, AboutDialog, KeyboardShortcutsHelp, AlertsIndicator, GlobalSearchDialog (the command palette) with its paletteCommands.ts, PrivacyToggle, PageHeaderSlot, NotificationItem, NotificationList, PaletteTrigger, PrivacyAnnouncer, navItems.ts |
planned/ |
8 | PlannedRow, PlannedCalendar, PlannedCalendarDayDialog, PlannedOccurrencesDialog, PlannedSuggestionsDialog, PlannedStatusBadge, PlannedAccountCell, SubscriptionsTab |
reports/ |
21 | FinancialIndependenceSection, FireChart, FireSettingsForm, CashFlowSection, CashFlowTable, SankeyChart, ReportBuilderSection, ReportDefinitionForm, ReportResult, ReportTooltipCard, SavedReportsPopover, RetirementSimulationSection, SimulationForm, SimulationChart, SimulationChart.utils.ts, reports.constants.ts (the server’s bounds, mirrored) |
savings/ |
5 | SavingsGoalForm — create/edit savings goal with account linking; TemplateFormDialog, GoalTemplateSelect, GoalProjection, TemplatesSection |
settings/ |
18 | ProfileCard, PasswordCard, AppearanceCard, CustomThemeCard, ApiTokensSection, WebhooksSection, NetWorthThresholdsSection/NetWorthThresholdForm, TotpSetup/TotpVerify, RecoveryCodesSection/RecoveryCodesDialog/RecoveryCodesList, SessionsCard/SessionRow, DangerZoneSection |
snapshots/ |
17 | SnapshotGridView, SnapshotGridRow, SnapshotGridCell, SnapshotEntryForm, SnapshotList, SnapshotRow, SnapshotRowEditDialog, SnapshotEditDialog, SnapshotDeleteConfirm, SnapshotDateRowDelete, SnapshotDateGroup, SnapshotEmptyState, SnapshotLoadingSkeleton, RecurringSnapshotsSection, AccountAmountInput, account-utils.ts (groupByInstitution, computeTotal) |
spendable/ |
4 | SpendableHeaderFigure, SpendableBreakdownDialog, SpendablePoolMenu, Spendable.utils.ts |
tour/ |
3 | TourProvider (offer and walk state), GuidedTour (the spotlight and step card), tourSteps.ts (the five steps and the anchor lookup) |
ui/ |
47 | shadcn/ui primitives (button, card, dialog, input, label, select, combobox, table, tabs, badge, alert-dialog, dropdown-menu, popover, separator, skeleton, sonner, tooltip) plus shared app-level UI: SortableGrid, SortableTableHead, ErrorState, EmptyState, LoadingSpinner, ListSkeleton, PageHeader, PaginationFooter, FilterBar, FilterToggle, ViewToggle, ConfirmDialog, BulkActionBar/BulkActionConfirm, EmptyTrashConfirm, PermanentDeleteConfirm, TrashListDialog, TruncatedBanner, CardActionsMenu, ChipsInput, BulkActionBar, BulkActionConfirm |
__tests__/ |
2 | Cross-cutting tests not colocated with a feature dir: FormatPreferencesSync.test.tsx, PwaUpdatePrompt.test.tsx |
| Root | 4 | ErrorBoundary.tsx, RouteErrorFallback.tsx, FormatPreferencesSync.tsx, PwaUpdatePrompt.tsx |
Type Files
Section titled “Type Files”34 TypeScript type files in src/types/ mirroring backend Pydantic schemas (one per domain; the main ones):
| File | Mirrors |
|---|---|
account.ts |
Account schema (includes LiquidityHorizon type) |
audit-log.ts |
AuditLog schema |
budget.ts |
Transaction + Budget schemas |
chart.ts |
Chart response schemas |
export.ts |
Export response schemas |
import.ts |
Import parse/commit schemas |
institution.ts |
Institution schema (includes is_default, archived_at, deleted_at) |
investment.ts |
Holding + Dividend schemas |
recurring-snapshot.ts |
RecurringSnapshot schema |
report.ts |
Report schemas: the financial independence projection, the cash flow graph, report definitions and runs, the retirement simulation, report settings and saved reports |
savings-goal.ts |
SavingsGoal schema |
snapshot.ts |
Snapshot schema |
user.ts |
User + auth response schemas |
admin.ts |
Admin user-management schemas |
alert.ts |
Alert, alert rule and notification preference schemas |
categorizationRule.ts, category.ts, subcategory.ts, tag.ts |
Rule, category, subcategory and tag schemas |
chart-annotation.ts |
Chart annotation schema |
csvCategoryImport.ts, csvImport.ts, ofxImport.ts |
Bank-file and category import schemas |
dashboard.ts |
Dashboard and card layout schemas |
envelope.ts |
Envelope and contribution schemas |
exchangeRate.ts, marketData.ts |
Exchange rate and market data schemas |
net-worth-threshold.ts |
Net worth threshold schema |
payee.ts |
Payee, PayeeRef and merge-preview schemas |
planned-suggestion.ts, planned-transaction.ts |
Planned transaction and suggestion schemas |
savedFilter.ts |
Saved filter schema |
subscription.ts |
Subscription view schema |
webhook.ts |
Webhook schema |
Routing
Section titled “Routing”React Router 8 with createBrowserRouter. The root path / is a public guard (LandingGuard) that shows a spinner while the sign-in status resolves, then redirects to /dashboard when signed in and to /login otherwise; it has no content of its own.
Authenticated routes are wrapped in a pathless RouteGuard layout route.
/help and /quickstart, including their sub-paths, are public routes that show a notice that the content moved to the documentation site; /help also links to it when the server publishes docs_url.
- Eager pages:
LoginPage,RegisterPage,ForgotPasswordPage,ResetPasswordPage,ConfirmEmailPage,AcceptInvitePage,NotFoundPage— imported directly inApp.tsx - Lazy pages: 29 pages loaded via
React.lazy(() => import(...))wrapped in<Suspense fallback={<PageLoader />}> - Route prefetch: A
routePrefetchmap insrc/routePrefetch.tstriggersimport()on hover, loading the chunk before navigation - Error boundaries: Each route has its own
errorElement: <RouteErrorFallback />for granular error recovery
export const routePrefetch: Record<string, () => Promise<unknown>> = { '/snapshots': () => import('@/pages/SnapshotsPage'), '/snapshots/new': () => import('@/pages/SnapshotEntryPage'), '/graphic': () => import('@/pages/ChartsPage'), // ... more entries};State Management
Section titled “State Management”TanStack Query 5.104 is the sole server-state manager. There is no Redux or Zustand.
- Query key pattern: Arrays like
['accounts', userId],['snapshots', { accountId, dateRange }] - staleTime: Configured per query to balance freshness and refetch frequency
- Lazy loading via
enabled: Chart, savings-goal, and transaction queries accept anoptions?: { enabled?: boolean }parameter;ChartsPagepassesenabled: activeTab === '<tab-name>'to avoid fetching data for hidden tabs - Mutations: Use
useMutationwithonSuccessinvalidation of related query keys; delete mutations accept optionalonSuccess/onErrorcallbacks for toast notifications - Bulk operations:
useBulkDeleteAccountsanduseBulkArchiveAccountscallapiFetchdirectly in parallel (bypassing per-item mutation lifecycle) and invalidate the cache once after all calls settle — avoids cascading refetch race conditions that overwrite optimistic updates - Cross-entity invalidation:
useDeleteInstitutioninvalidates['institutions'],['accounts'],['snapshots'], and['charts']because soft-deleting an institution cascades to linked accounts and their snapshots;useRestoreInstitution,usePermanentDeleteInstitution, anduseEmptyInstitutionsTrashalso invalidate the['institutions', 'deleted']query key to refresh the trash list - apiFetch: All queries and mutations go through the centralized
apiFetchwrapper (see API Client below)
Internationalization is handled by react-intl with an I18nProvider wrapper.
- Module structure: The
I18nProvidercomponent lives insrc/i18n/I18nProvider.tsx. Non-component exports (Localetype,getStoredLocale,intlLocale,useLocaleContext,LocaleContext) are insrc/i18n/locale.ts. The barrelsrc/i18n/index.tsre-exports from both. - 5 locale catalogs:
fr.json,en.json,es.json,de.json,it.json(the same keys in every file) insrc/locales/ - Locale detection: User profile →
localStorage→navigator.language→fr(fallback) - Locale sync: Changing locale in the UI calls
PATCH /api/v1/auth/mewith the new locale, persisting it server-side - Language switcher:
LanguageTogglein sidebar footer cycles FR → EN → ES → DE → IT; full selector on Settings page - HTML lang attribute: Updated on locale change via
useEffect - Format locale sync:
setFormatLocale()called on locale change soformat.tsutilities use the current locale without prop drilling - Intl locale mapping:
fr→fr-FR,en→en-US,es→es-ES,de→de-DE,it→it-IT(forIntl.NumberFormatanddate-fns) - Key-parity test:
locale-parity.test.tsverifies all five locale files have identical keys and no empty values - Zod validation: Error messages store i18n keys, rendered via
<FormattedMessage>— no hardcoded French in schemas - Date/number format presets:
date_format/number_formatare independent per-user display preferences (separate from the UI-languagelocale) — fixed preset dropdowns in Settings, synced app-wide viaFormatPreferencesSync(mounted at the app root, insideAuthProvider) intolib/format.ts’s module state.resolveNumberLocale()is the seam chart components must use instead ofintlLocale()directly for currency/number formatting, so the preference is honored even by hand-rolledIntl.NumberFormatcall sites.
Privacy Mode, Command Palette and Guided Tour
Section titled “Privacy Mode, Command Palette and Guided Tour”Privacy mode hides amounts on screen for one device. It is display-only: no API request or response, server-generated export file (CSV, XLSX, ODS, backup) or stored server data changes, and it is not a security feature. The one deliberate exception is the PDF summary report, which is built in the browser through formatCurrency and so comes out masked.
- Store.
lib/privacy.tsis a leaf module holding a module-level flag, persisted inlocalStorageunderpatrimoine-privacy, withisPrivacyOn,setPrivacy,togglePrivacyandsubscribePrivacy. Astorageevent listener makes a second open tab follow the first without writing back.usePrivacyModereads it withuseSyncExternalStore. - Masking. Every amount goes through
lib/format.ts:formatCurrencyandformatCompactCurrencyreturn the mask••••,maskAmountmasks text built elsewhere, andformatAxisTickmasks chart axis ticks. - The re-render seam.
formatCurrencyreads module state that no component subscribes to, so flipping the flag would leave already-rendered text untouched.I18nProvidertherefore passesIntlProvideraformatsprop that is one of two constant objects, chosen by the flag. react-intl rebuilds itsintlobject when a config prop changes by reference, which re-renders everyuseIntlconsumer,React.memorows included. - Ratchet test.
lib/__tests__/privacy-ratchet.test.tsscans the app source for amounts formatted behind the formatters’ back (new Intl.NumberFormat(,.toLocaleString(, a raw{row.amount}render) and fails on any hit outside its short, justified allow-list.
Command palette. GlobalSearchDialog is the Ctrl+K dialog: one combobox over one listbox with two groups, a “Commands” group above the server search results. The commands come from buildPaletteCommands in paletteCommands.ts, a static code-defined list (a go-to command for every page, the mode toggle and the privacy toggle). The pages come from navItems.ts, which the sidebar shares: its NAV_GROUPS (the sidebar), EXTRA_PAGES (the profile-menu pages) and SETTINGS_SECTIONS (one command per Settings card). PAGE_META is derived from it, NAV_SHORTCUTS only decides which pages carry a shortcut hint, and a Settings section is reached through a #settings-<slug> anchor that SettingsPage scrolls to and focuses. paletteCommands.test.ts fails when a sidebar page, a route of App.tsx or a Settings card has no command; the routes that deliberately have none sit in an allowlist, each with its reason. The typed text only filters the command labels, using foldForSearch to ignore case and accents; it never becomes a route or code.
Guided tour. TourProvider holds the state of both surfaces: the one-time offer on the dashboard, derived from the signed-in user’s tour_seen flag, and the five-step walk. Answering the offer writes the flag first through useMarkTourSeen, then optionally starts the walk; relaunching from the profile menu starts the walk and never touches the flag. GuidedTour renders the spotlight and step card. tourSteps.ts lists the steps, each pointing at a data-tour attribute on a real element (no wrapper DOM). The same anchor can appear twice (sidebar and bottom bar), so resolveTourAnchor picks the first element that is actually laid out, and steps are resolved once at start, so on a phone the walk covers only what is visible.
Split, merge and saved filters. SplitDialog and MergeDialog are mounted by BudgetPage only while a row or pair is selected, and call useSplits and useMergeTransactions. Saved filters keep the page’s UI-shaped filter state (cat-N / sub-N / uncategorized picks, two type toggles, one account) out of the API through a single pair of pure functions in lib/saved-filters.ts: fromFilterState builds the payload to store, and toFilterState turns a stored payload back into page state, dropping ids that no longer exist in the loaded categories, subcategories, accounts and tags and keeping the first known account. Both are covered by unit tests, so the page never serialises its own state.
Reports. ReportsPage is a lazy route at /reports with one tab per section (financial independence, cash flow, report builder, retirement simulation). ?section= mirrors the active tab: an unknown value falls back to the first tab and changing tab replaces the history entry. Only the active tab’s section is mounted, so its queries (useFireProjection and useReportSettings, useCashFlow, or the saved reports list) fire only while it is on screen, and a simulation runs only when the user presses Run. Every figure comes from the server: the projection, the cash flow graph and a report run are computed by the backend, and the client formats and draws them without recomputing a total. In the builder, ReportBuilderSection owns the working definition and the last result (the run together with the definition that produced it); the result is flagged out of date while definitionsEqual says the form differs from it, and opening a saved report passes its definition through normalizeDefinition and runs it at once. ReportDefinitionForm.utils.ts mirrors the server’s rules (which charts a grouping allows, a second grouping that differs from the first) for convenience only, because the server’s 422 is the authority. Privacy mode masks money only: headlines, assumptions, tooltips, axis ticks, tables and chart names go through formatCurrency, while names, shares and counts stay visible, and the CSV exports keep the real values. The charts use the existing Recharts chunk.
API Client
Section titled “API Client”All API calls go through three centralized functions in src/api/client.ts:
| Function | Purpose | Key Details |
|---|---|---|
apiFetch<T> |
Standard JSON requests | credentials: 'include' (sends HTTP-only JWT cookie), sets X-Requested-With: XMLHttpRequest header, auto-sets Content-Type: application/json for body requests, returns undefined for 204 |
apiUpload<T> |
FormData file uploads | Does NOT set Content-Type (browser sets multipart boundary), same credentials pattern |
apiDownload |
File downloads | Fetches as blob, validates Content-Type against allowlist (text/csv, application/json, application/vnd.oasis.opendocument.spreadsheet, application/octet-stream), creates <a download> element |
401 Handler
Section titled “401 Handler”A global handler in src/main.tsx (QueryCache/MutationCache onError) calls window.location.replace('/login') on a 401, forcing a full page reload to clear stale client state. It skips public routes, the credential errors (invalid_password, totp_incorrect_password, totp_invalid, totp_required) and oidc_provider_unavailable, which the calling form or RouteGuard handles itself.
The app is installable as a Progressive Web App.
There is no hand-written public/manifest.json or public/sw.js — both are generated at build time by vite-plugin-pwa (VitePWA({ manifestFilename: 'manifest.json', registerType: 'prompt', ... }) in vite.config.ts). See PWA for the full generated-manifest/service-worker behavior, caching strategy, and the registerType: 'prompt' update flow.
| File | Purpose |
|---|---|
public/theme-init.js |
Script loaded before React — reads localStorage('skin') and sets the data-skin attribute on <html> before paint, preventing a flash of the wrong skin (CSP-compliant, no inline event handlers) |
public/locale-init.js |
Script loaded before React — reads the stored language and adds a modulepreload for that catalog from the locale-chunks meta tag the build writes into index.html, so the catalog downloads alongside the entry chunk (external file for the CSP; its language list is mirrored from LOCALE_LOADERS and guarded by locale-init-sync.test.ts) |
Bundle Splitting
Section titled “Bundle Splitting”Vite manualChunks configuration splits vendor code into separate chunks for better caching:
| Chunk | Contents |
|---|---|
recharts |
Recharts charting library |
react-vendor |
react, react-dom, react-router, scheduler, clsx |
dnd-kit |
@dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities |
query |
@tanstack/react-query |
base-ui |
@base-ui/react, @radix-ui/react-slot |
icons |
lucide-react |
date-fns |
date-fns |
forms |
react-hook-form, zod, @hookform/resolvers |
Related Documents
Section titled “Related Documents”- Frontend Patterns — Component conventions, naming, file organization
- i18n Specification — Locale catalogs, key naming, backend sync
- PWA Specification — Service worker strategy, manifest, offline behavior
- Design System — Tailwind theme, color tokens, typography