Skip to content

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 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

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

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

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 in App.tsx
  • Lazy pages: 29 pages loaded via React.lazy(() => import(...)) wrapped in <Suspense fallback={<PageLoader />}>
  • Route prefetch: A routePrefetch map in src/routePrefetch.ts triggers import() on hover, loading the chunk before navigation
  • Error boundaries: Each route has its own errorElement: <RouteErrorFallback /> for granular error recovery
src/routePrefetch.ts
export const routePrefetch: Record<string, () => Promise<unknown>> = {
'/snapshots': () => import('@/pages/SnapshotsPage'),
'/snapshots/new': () => import('@/pages/SnapshotEntryPage'),
'/graphic': () => import('@/pages/ChartsPage'),
// ... more entries
};

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 an options?: { enabled?: boolean } parameter; ChartsPage passes enabled: activeTab === '<tab-name>' to avoid fetching data for hidden tabs
  • Mutations: Use useMutation with onSuccess invalidation of related query keys; delete mutations accept optional onSuccess/onError callbacks for toast notifications
  • Bulk operations: useBulkDeleteAccounts and useBulkArchiveAccounts call apiFetch directly 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: useDeleteInstitution invalidates ['institutions'], ['accounts'], ['snapshots'], and ['charts'] because soft-deleting an institution cascades to linked accounts and their snapshots; useRestoreInstitution, usePermanentDeleteInstitution, and useEmptyInstitutionsTrash also invalidate the ['institutions', 'deleted'] query key to refresh the trash list
  • apiFetch: All queries and mutations go through the centralized apiFetch wrapper (see API Client below)

Internationalization is handled by react-intl with an I18nProvider wrapper.

  • Module structure: The I18nProvider component lives in src/i18n/I18nProvider.tsx. Non-component exports (Locale type, getStoredLocale, intlLocale, useLocaleContext, LocaleContext) are in src/i18n/locale.ts. The barrel src/i18n/index.ts re-exports from both.
  • 5 locale catalogs: fr.json, en.json, es.json, de.json, it.json (the same keys in every file) in src/locales/
  • Locale detection: User profile → localStorage → navigator.language → fr (fallback)
  • Locale sync: Changing locale in the UI calls PATCH /api/v1/auth/me with the new locale, persisting it server-side
  • Language switcher: LanguageToggle in 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 so format.ts utilities 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 (for Intl.NumberFormat and date-fns)
  • Key-parity test: locale-parity.test.ts verifies 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_format are independent per-user display preferences (separate from the UI-language locale) — fixed preset dropdowns in Settings, synced app-wide via FormatPreferencesSync (mounted at the app root, inside AuthProvider) into lib/format.ts’s module state.
    • resolveNumberLocale() is the seam chart components must use instead of intlLocale() directly for currency/number formatting, so the preference is honored even by hand-rolled Intl.NumberFormat call 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.ts is a leaf module holding a module-level flag, persisted in localStorage under patrimoine-privacy, with isPrivacyOn, setPrivacy, togglePrivacy and subscribePrivacy. A storage event listener makes a second open tab follow the first without writing back. usePrivacyMode reads it with useSyncExternalStore.
  • Masking. Every amount goes through lib/format.ts: formatCurrency and formatCompactCurrency return the mask ••••, maskAmount masks text built elsewhere, and formatAxisTick masks chart axis ticks.
  • The re-render seam. formatCurrency reads module state that no component subscribes to, so flipping the flag would leave already-rendered text untouched. I18nProvider therefore passes IntlProvider a formats prop that is one of two constant objects, chosen by the flag. react-intl rebuilds its intl object when a config prop changes by reference, which re-renders every useIntl consumer, React.memo rows included.
  • Ratchet test. lib/__tests__/privacy-ratchet.test.ts scans 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.

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

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)

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