This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Folio is a personal finance tracker (Next.js 14 App Router + TypeScript) for investments, bank deposits, and retirement funds. All data is stored locally in a SQLite database via Prisma.
# Install dependencies
npm install
# Initialize the database (creates prisma/dev.db)
npx prisma db push
# Development server (binds to 0.0.0.0 for LAN access)
npm run dev
# Production build
npm run build
# Lint
npm run lint
# Run tests (Vitest)
npm test
# Inspect database interactively
npx prisma studio
# After schema changes, regenerate Prisma client
npx prisma generateTests use Vitest (npm test); they live next to the code as *.test.ts files under src/.
All data access is centralized in a single Next.js Server Actions file: src/app/actions.ts. This file handles every read and write operation — there are no separate API routes for data (the only route is the Google OAuth callback at src/app/api/auth/google/callback/route.ts).
src/lib/db.ts— Prisma singleton (prevents multiple client instances in dev HMR).src/lib/server-services.ts— Isolates server-only Node.js imports (yahoo-finance2,googleapis,fs,DB_PATH) so they never leak into Client Components.src/lib/fx.ts— historical EUR/USD FX rates: daily ECB rates synced from the Frankfurter API intoHistoricalPrice(symbolEURUSD=X, price = USD per 1 EUR), plusconvertCurrency/fxRateForDatelookup helpers used by every currency-converting server action. Synced fire-and-forget on/and/investmentspage loads.
Single Asset table with a type discriminator:
STOCK/ETF/CRYPTO— market-priced assets withTransactionrowsDEPOSIT— bank deposits; principal stored asTransaction.price(quantity = 1)PENSION— retirement funds;manualPricefield stores current unit price
HistoricalPrice stores end-of-month prices per symbol for the performance chart. Setting stores key-value app configuration (API keys, backup path, OAuth credentials) that can override environment variables.
Priority: Twelvedata → FMP → Yahoo Finance → DB cache (last known price).
An in-memory cache (5 min TTL) sits in front of all providers. ISINs are resolved to ticker symbols via Yahoo Finance search, with a 24h ISIN cache and a hardcoded ISIN_OVERRIDES map for known problematic ISINs (e.g. Xiaomi Frankfurt).
Monthly performance uses the Modified Dietz method. getAssetDetails computes per-symbol performance; getPortfolioPerformance aggregates all assets into a global view. Historical prices are synced lazily in the background on each /investments page load (syncHistoricalPrices), throttled 2–5 s between symbols to avoid Yahoo Finance 429s.
| Route | Component | Purpose |
|---|---|---|
/ |
src/app/page.tsx |
Global dashboard — net worth across all asset types |
/investments |
src/app/investments/page.tsx |
Stock/ETF/Crypto portfolio |
/investments/[symbol] |
src/app/investments/[symbol]/page.tsx |
Per-asset detail + monthly performance table |
/deposits |
src/app/deposits/page.tsx |
Bank deposit tracker |
/pension |
src/app/pension/page.tsx |
Retirement fund tracker |
Currency switching (?currency=EUR or ?currency=USD) is a URL query param propagated between pages.
Client-safe type definitions used by both Server Actions and Client Components: TransactionData, DepositData, PensionData, Holding.
Two broker import parsers, each with a matching pair of Server Actions in actions.ts:
trade-republic-parser.ts— parses Trade Republic PDF bank statements. Useseval('require')('pdf-parse')to bypass Next.js bundler restrictions on the pdf-parse module. Called byparseTradeRepublicStatement/importTradeRepublicStatement.xtb-parser.ts— parses XTB broker Excel (.xlsx) exports using thexlsxlibrary. Reads theCash Operationssheet (rows start at index 5). XTB tickers use exchange suffixes (e.g.CDR.PT) that are stripped to canonical symbols (CDR) viaXTB_EXCHANGE_SUFFIX_MAPinactions.ts. Called byparseXTBStatementAction/importXTBStatementAction.
getQuotes in actions.ts normalises three symbol formats before fetching:
- ISIN (regex
/^[A-Z]{2}[A-Z0-9]{9}\d$/) — resolved to ticker via Yahoo Finance search with 24h cache; hardcoded overrides inISIN_OVERRIDESfor known problem cases (e.g. Xiaomi Frankfurt1810.F). - XTB exchange-suffix (regex
/^[A-Z0-9]+\.(US|PT|ES|IT|FR|BE)$/) — normalised byresolveXTBTickerusingXTB_EXCHANGE_SUFFIX_MAP/XTB_TICKER_OVERRIDES. - Plain ticker — passed through unchanged.
getPortfolio()— returns current holdings (net quantity > 0).getSoldPortfolio()— returns fully-exited positions (net quantity = 0), shown inClosedPositionsTableon the investments page.
All dialogs and interactive components in src/components/ are 'use client'. They call Server Actions directly (not via fetch). Notable ones:
DataManagementDialog— local export/import, local path backup, and Google Drive backup/restoreImportStatementDialog/ImportButton— Trade Republic PDF statement importImportXTBDialog/ImportXTBButton— XTB Excel statement importHoldingsTable— active holdings with inline asset name editing viaupdateAssetNameClosedPositionsTable— fully-exited positions (read-only)
Tailwind CSS v4, Recharts for charts, Lucide React for icons. No component library — custom components in src/components/ui/ (currently only progress.tsx).
| Variable | Source | Purpose |
|---|---|---|
DATABASE_URL |
.env |
SQLite file path (e.g. file:./dev.db) |
GOOGLE_CLIENT_ID |
.env or DB Setting |
Google OAuth for Drive backup |
GOOGLE_CLIENT_SECRET |
.env or DB Setting |
Google OAuth for Drive backup |
NEXT_PUBLIC_BASE_URL |
.env or DB Setting |
Callback URL base (e.g. http://localhost:3000) |
TWELVEDATA_API_KEY |
DB Setting only | Optional; preferred quote provider |
FMP_API_KEY |
DB Setting only | Optional; second-choice quote provider |
API keys for Twelvedata and FMP are stored in the Setting table via the Data Management dialog, not in .env.