Skip to content

About

Track your net worth and investment portfolio with automatic price updates, asset allocation, expense tracking, and FIRE calculator. Modern alternative to spreadsheets. Next.js + Firebase + TypeScript.

Topics

Resources

Stars

77 stars

Watchers

3 watching

Forks

Latest commit

 

History

1,437 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Net Worth Tracker

Next.js React TypeScript Tailwind CSS Firebase Vitest License

Description

Net Worth Tracker is a full-featured personal finance application built for Italian investors. It provides comprehensive portfolio tracking, performance analytics, cashflow management, dividend monitoring, and long-term financial planning tools — all in a single dashboard.

The app integrates with Yahoo Finance for real-time price updates and includes advanced features like Monte Carlo simulations, FIRE (Financial Independence, Retire Early) projections, AI-powered performance analysis, and a dedicated AI assistant foundation via Claude. The UI is in Italian while the codebase follows English conventions.

Key Features

Portfolio Management

  • Overview that answers before it counts — the Panoramica opens with a one-sentence verdict on the month ("Agosto sta andando bene." / "Agosto è in calo, nonostante il mercato." / "Settembre è in calo: il mercato ha pesato, le tasse sulle vendite di più." / "Settembre è in calo per le tasse sulla vendita di VWCE, non per il mercato." / "Settembre è in pari: le tasse sulla vendita di VWCE si sono prese la crescita."), generated from your data and never claiming what the data can't support — a falling month names the market only when prices fell, not the market alone when the tax withheld on a sale you recorded in the ledger, or your own outflows, weighed more, and the sale itself when the market gained and that tax explains the drop — or when a month that did not fall owes its flatness to that tax; the sentence under it splits the change into what the market did (the quotes you bought or sold during the month included, from their trade price), what you saved — income minus expenses already happened — and what else moved, gives the savings rate so far with what is still in the calendar, names the class that moved the market most, and tells the sale in the past tense with the tax estimated from the instrument's rate, what the month would have made without it («senza, il mese avrebbe fatto +4213 € (+2018 € dal mercato e +2095 € risparmiati)») and what you bought in the same month — followed by a grid of tiles that each answer one question with a short reading above the figures: net worth with trend and a "Mercato" line (the price effect on what you held, and on what you traded this month from the trade price — the money you move in or out never counts as return), liquidity split, this month's cashflow with an end-of-month spending projection, composition, costs with the instruments that weigh most, up to three goals, spending and income by category (each row opens that category's Scheda on Analisi), and your largest positions — every tile closing on the page that owns the depth
  • Multi-asset tracking across stocks, ETFs, bonds, crypto, real estate, commodities, pension funds, and cash — added via a guided two-step dialog: pick the asset type first, then fill in only the relevant fields for that type
  • Multi-currency support: assets priced in USD, GBP, CHF, etc. are automatically converted to EUR for all portfolio calculations using live Frankfurter exchange rates; LSE pence (GBp) normalized to GBP automatically
  • Automatic price updates via Yahoo Finance (all assets) and Borsa Italiana (Italian bonds with ISIN)
  • Ticker display alias: give any investment a short, readable label (e.g. "CL2" instead of "CL2.MI") shown everywhere across the app instead of the raw ticker — automatic price updates keep using the real ticker underneath, unaffected
  • Bond coupon scheduling: automatic coupon generation with step-up rate tiers and final premium (Premio Finale) support — full BTP Valore compatible; a single coupon paid with the redemption («Unica a scadenza», the BTP Valore Insieme), the annual rate compounded over the bond's life and previewed in the form; inflation-linked coupons for BTP Italia (FOI added per period) and BTP€i (real rate × the HICP indexation coefficient you enter, which also scales the market value); a zero-coupon bond saves its details and generates nothing
  • Average cost tracking with 4-decimal precision, including a built-in multi-broker PMC calculator for positions spread across multiple brokers. Every gain, tax estimate, yield on cost and sale simulation is measured against the euro cost of the position at the trade-date rates, purchase fees included — the fiscal cost — so a foreign-currency instrument is never measured against its own-currency PMC; the table prints that euro PMC and, on a foreign-currency row, the native one under it
  • Operations register (Registro operazioni): record explicit Buy / Sell / Adjustment operations on each tracked investment (stocks, ETFs, bonds, crypto, commodities), with an optional settlement account whose balance updates automatically — operations are net-worth-neutral (money simply moves between the asset and your cash). A per-asset "Movimenti" view lists your full operation history alongside realized P&L, total return, and money-weighted return (XIRR); each sale row also shows its realized gain/loss as a percentage and the average cost (PMC) at the moment of that sale. A sale shows an estimated realized-P&L preview before you confirm, and a "Tasse trattenute" field prefilled with the estimated capital-gains tax (the price difference at the instrument's rate, as a broker in regime amministrato computes it) that you can overwrite with the figure on your statement: the settlement account receives proceeds less fees less that tax, to the cent, and the window prints the credit before you save. An operation carries its real date, however old: shares bought years before you opened the register are recorded on the day you bought them — the only limits are the future and, for a position that already existed when the register opened, that opening day, which the form names; with a date in a past month the form warns that the settlement account is debited today, not on that date. For tracked investments, quantity and average cost are managed through the register, so editing an asset can never overwrite its cost basis. The register also powers "Capitale investito" and "Plusvalenze realizzate" on Performance and the total-return breakdown on Dividends (see below)
  • Fondo Pensione (Previdenza): track your Italian complementary pension as a manually-valued asset — no ticker, valued from your statement with the page's own «Aggiorna valore» action (the tile's footer says when the value was left in a closed month), with its own equity/bond mix. The Previdenza page (redesigned 2026-08-26) opens with a one-sentence verdict on whether the fund is working — "Il fondo sta lavorando." / "Il fondo ha perso terreno." / "Il rendimento del fondo non è misurabile." — followed, for each contributor, by the three causes of growth as three numbers: what the market returned on the trusted window (a TWR, net of contributions), what the employer added in the year and what the tax office gives back; a cause with nothing behind it drops its clause, and a return that cannot be measured is said as such instead of a percentage — too high for the contributions recorded (something is missing), or contradictory, with more contributions recorded than growth (something was counted twice, or was already inside a hand-entered value); the page tells the two apart because the advice differs. Then five tiles: the fund today (its live value, this month's market effect — the same figure the Overview's digest prints — everything ever paid in, the value series closed on today's value), the return (TWR, annualised, market gain, the return on your own capital with the employer's share kept apart — employer contributions are pay, not investment return), the fiscal year (what was deducted against the ceiling, the estimated IRPEF saving, the "extra-deducibilità" plafond if you're eligible), the year's contributions by nature and the ledger of the year with a two-click delete that reverses the contribution's effect (the row says what the delete undoes before you confirm). Recording a contribution asks for the fiscal year as a choice around the payment date, and its confirmation names the next step: update the value when the statement arrives, because the statement already includes it. The fiscal year sits beside the verdict and drives the annual tiles; the fund's value and its return keep their own windows. Contributions are TFR, employer, or a voluntary payment debited from a linked cash account; an existing tracked investment can be converted into a pension fund without losing its value or history. If you track more than one person's pension fund in the same account (e.g. both spouses), add each as a family member in Settings — the verdict, the return and the tax-saving estimate are computed once per person against their own funds and income, never mixed. Your pension fund counts toward your true asset-class exposure on Allocazione (it's locked, not tradable, so no plan ever buys or sells it) and has its own Previdenza tile there; Storico shows it as a dedicated "Previdenza" band; and Performance metrics leave it out by default, since it's capital fed by contributions rather than market activity — switch «Includi i fondi pensione» on and the fund counts from the month its contributions are recorded, with every contribution treated as capital paid in rather than return, so only the fund's own market effect reaches your TWR. Pick the month your contributions became fully recorded in Settings; before it, contributions and growth are indistinguishable and no return is shown. One thing to know: a contribution is attributed to the month you record it, so if your fund credits contributions with a lag and you copy its value from the website every month, the month you record reads the market a little low and the month the fund credits it a little high — the window's total is right as soon as the fund credits it and you update the value.
  • Current vs target asset allocation visualization
  • Automatic Azioni/Obbligazioni targets (opt-in, Settings → Allocazione): enter your age and the risk-free rate and The Bull's rule of thumb (125 − age − rate × 5) sets the equity share. Obbligazioni take the formula's residual and stay there; every other class you allocate — materie prime, crypto, immobili, trend following, carry — is funded out of the Azioni side, so a satellite sleeve never eats into the defensive part of the portfolio. The summary line states all three figures, and the targets always total 100%
  • Asset allocation as a verdict over tiles (redesigned 2026-08-25): the page opens with one sentence — "Allineato al 96%." — followed by the facts in words (which classes drift and by how many points, the leverage against its target when there is one, and where the next 1000 € would go), then a grid of tiles. Bilanciamento: the band-independent balance score as a ring, the share out of position, the classes off target under the drift threshold you pick right there (±2% / ±5% / the 5/25 rule / custom — it reclassifies the chips, never the score), the current mix over the target mix as two bars on one legend, and what the total holds but cannot move and what it leaves out. Piano: Ribilancia (what to buy and trim, down to the individual instruments to trade), Versa (a no-sell contribution planner: where new cash goes, drilling class → sub-category → the instrument to buy, following your specific-asset targets) and Preleva (the decumulation mirror: "I want €X in hand, what do I sell?" — it draws first from whatever sits above target, down to the instrument and the position you'll be left holding) behind one switch, the amount shared with the verdict. Every plan that sells prices the capital-gains withholding: «Per prelevare 1000 € netti vendi 1087 €… la ritenuta stimata è 87 €», estimated on the gain of the slice being sold — and dropped rather than guessed when an instrument carries no cost basis or rate. Per classe: every class on one line — current and target share, the gap in euro, a tick that shows where it sits — with sub-categories and theoretical specific-asset targets inline; a sub-category is optional on an asset, and inside a class where you set sub-category targets whatever carries none is listed last as "Senza sottocategoria" — its value still counts in the class, it simply has no target of its own: no gap, no COMPRA/VENDI chip, and the contribution plan never sends new money there (a withdrawal can still draw from it pro-rata, like any untargeted sleeve). Then Esposizione and Previdenza, and the non-tradable and the excluded holdings below behind a "Dettaglio" disclosure. COMPRA / VENDI / OK action chips; colors follow the selected theme
  • Per-asset allocation roles — not all wealth can be traded in slices, and the allocation math now knows it. Each asset is Ribilanciabile (normal), Non negoziabile (a locked pension fund or private equity: it is invested wealth, so it counts in your percentages and your true equity/bond exposure is right — but no plan ever buys or sells it, and the plans instead reach your target by moving what you can move), or Escluso dall'allocazione (the home you live in: not an investment, so it leaves the page entirely). Plans only ever propose amounts you can actually execute — a sell is capped at your tradable holdings — and the page flags any target left stranded with nothing tradable behind it
  • Leveraged & composite ETFs: set a leverage multiplier on an ETF (e.g. 2 for a 2x fund) and the allocation page reasons about your notional exposure — the real risk you carry — not just market value. When you hold leverage, the verdict and the Bilanciamento tile state the current leverage against the target one, the composition bar's labels sum to your leverage, and your per-class targets in Settings can sum above 100% to express a target leverage (shown as e.g. "Leva target 1,50×"). The Ribilancia / Versa / Preleva plans then reason over your actual instruments — buying €1 of a 2x fund moves more than €1 of exposure — instead of assuming plain purchases. With no leverage anywhere, everything looks exactly as before. In Patrimonio › Strumenti a composite instrument (a 60/40 fund, a balanced ETF) stays one row, but its class chip shows every class it holds — one segment per class, as wide as its share and in that class's colour, «Azioni · Obbl.» for two, «Misto» for three or more; a class under 5% gets no segment, a screen reader hears every share, and the group headers and the sort by class keep the prevailing class
  • Portfolio exposure breakdown: an Esposizione tile on the Allocation page that aggregates underlying-company, sector and issuer exposure across the funds and stocks of your Allocazione portfolio — the six heaviest as ranked rows, closed by «Resto letto» (what the read compositions do not name) and «Non letto», so the list adds up to 100 — under a coverage line that says where every euro went: read, unread with the instrument's name (a synthetic ETF Yahoo has nothing for), not applicable by nature (gold, crypto, a property, a pension fund), or outside this view (the bond sleeves in Titoli and Settori). Titoli and Settori weigh the NOTIONAL of the equity sleeves — a 2× fund moves twice its price, a 60/40 fund enters with its equity leg only — and Emittenti the market value of every quoted instrument once; the aside names the base of the percentage column and the reading names the heaviest holding, the first sector and the biggest issuer on it. Tap a row to see the instruments behind it with the formula per source ("X% di €Y = €Z") and a total. Yahoo's answers live in ONE cache shared by every account, per ticker (30 days for a useful answer, 24 hours for an empty one), the weighing runs in the browser on the page's own assets, and a delegated member of a shared account sees the owner's exposure; «Aggiorna» asks Yahoo again and the footer dates the oldest answer used
  • Current-year historical tables use a hidden previous-month baseline so January can show growth vs the previous December without rendering an extra visible column
  • Patrimonio opens with a one-sentence verdict on your portfolio — "Il portafoglio cresce." / "Il portafoglio è in calo: il mercato ha pesato." / "…le tasse sulle vendite di più." — followed by the facts in words (value, change over last month, how many instruments and accounts you hold, the single instrument that moved the most, the split into market, savings and other changes and, when you sold, the sale with its estimated tax, the month without it and what you bought beside it — the same rule as the Panoramica, whose headline names the tax when it took the month's growth), then a grid of tiles: net worth with trend and a per-instrument market digest, your cash accounts one per row, this month's buys and sells from the operations register, the asset-class mix, your unrealized gain with the best and worst positions, and the instruments table — still a real management table with column sorting (Valore, G/P, Peso, Nome, Classe and the three Δ), a group-by-class toggle, the ticker under each name — a bond adds its maturity and next coupon («scade il 10/03/2032 · prossima cedola 10/12»), a hand-valued holding when its value was last typed — and an "Andamento" view that swaps Quantità · Prezzo · PMC · TER for the Δ month / YTD / since-start columns (both toggles are remembered). A debt is an asset of its own, worth minus what you still owe: a «Prestito» — a mortgage linked to the property it finances, or a personal loan — keeps the house at its gross value and sits beside it in the Immobili class (a personal loan is negative liquidity, like a credit card, and reads «debito» among the accounts); its TAN lets the linked instalments lower the debt by their principal on their own dates, an early repayment («Estinzione anticipata», partial or total) goes all to principal, and a loan repaid in full reads «Estinto». A mortgage recorded the old way, as a field of the property, becomes a loan by itself the first time you open Patrimonio. A «Mutuo» tile (or «Prestito») then answers «how much does the loan cost?», one per loan: the interest and principal the linked instalments paid this year, the debt, when the plan ends at today's pace (French amortisation) and, from the second year, a year-by-year table — counted from the first linked instalment, never reconstructed before it. Those Δ columns measure the unit price of each position (in EUR, real estate gross of any mortgage), never its total value, so buying more of something never reads as performance; pension funds, properties and cash accounts, whose quantity is the value, show "—" and carry no G/P. Every delete is a two-click confirm without a timer, and deleting a cash account says in words what is lost before the second press. On the phone the instruments are flat rows that expand in place on the details, the price trend and the actions
  • Holdings with no market quote — cash accounts, real estate, private equity and pension funds, plus anything you've switched off automatic updates for — carry a subtle row (and card) tint, so you can tell at a glance which values you're responsible for keeping up to date. It marks manual pricing, not illiquidity

Performance Analytics

  • Rendimenti opens with a one-sentence verdict — "Nell'ultimo anno il portafoglio rende più del 60/40." / "…rende, ma il rischio pesa." / "…perde." — followed by the facts in words (the TWR, the gap against the 60/40 in points, the Sharpe, the deepest drawdown with its month and how long it took to recover, how many months were positive) and the measured basis named under it, then a grid of tiles: the return with the growth of 100 € against the 60/40; volatility, Sharpe, Sortino and the max drawdown with its months; the heatmap of monthly returns; the capital that entered the measured base — the one figure every return formula removes — with the channels that add up to it, and the register's buys and sells and the cashflow's savings beside it as terms of comparison (an opening position of the register migration is never counted as a purchase); the six model portfolios ranked in EUR; realized gains per fiscal year; where the return came from — the period's market gain instrument by instrument, in euro, with the dividends each one paid, closed by what no instrument explains so the rows visibly add up to the market's figure, and naming any month where that unexplained part exceeds 2% of the base, so you know where to look; and the invested base under the net worth. The fifteen metrics, the full attribution table, the rolling charts, the underwater chart and the method sit below behind a "Dettaglio" disclosure, with metric definitions in inline popovers; every tile's method sits behind «Come si calcola», and closing «Analizza con AI» cancels the analysis
  • The verdict names the window actually measured ("Negli ultimi 11 mesi" when a year finds eleven monthly snapshots, "Da aprile" for a year-to-date that starts there) and every gap beside a figure is on that figure's basis; the period selector (YTD / 1 anno / 3 anni / 5 anni / Storico) sits beside the verdict, and a custom date range is a dismissible chip under it, never a tab that looks disabled
  • Yield on Cost (YOC) and Current Yield calculations
  • Monthly returns heatmap in the app's sign colours (three intensities whose thresholds the legend prints, so it follows all six themes) — a month is read by tapping its cell or by reaching the grid with Tab and moving with the arrows, its return printed on a line under the grid — and underwater drawdown chart. Both read the same series: the max drawdown, its duration, its recovery and the Underwater chart all chain the monthly returns shown in the heatmap, so the chart is exactly the compounding of the months above it and the percentages don't shift as your contributions accumulate
  • The period transforms instead of being replaced (2026-09-12): switching YTD → 1 anno → Storico keeps every tile on screen — the TWR glides from the old figure to the new, «Crescita di 100» and «Capitale e mercato» bend into the new window, the heatmap fades cell by cell and its years appear or leave, and «Da dove viene il rendimento» re-ranks its rows and slides its bars. Only «Aggiorna», which re-reads the data, dims the page while it waits
  • Configurable calculation basis (Settings → Preferenze): performance metrics measure the portfolio you actively manage, so pension funds and assets excluded from your allocation (typically your home) are left out by default — either can be brought back in, and the page states which basis it is using. Bringing a pension fund in is honest by construction: it enters the base from the month its contributions are recorded (the page names that month), its entry and every later contribution count as capital paid in, never as return, and the pension switch decides on its own even for a fund marked «escluso dall'allocazione». Leaving a hand-valued property out raises measured volatility and lowers Sharpe, because a value that never moves between updates quietly damps your measured risk
  • Honest measurement rules: the first snapshot of a period is the starting valuation, so the measured window opens the month after it and your first month's savings are never counted as return; below a year the headline states the period return instead of an annualized one (extrapolating a year from a few months is a forecast, not a measurement; from six months on the annualized rate sits beside it, labelled); volatility and Sharpe show "—" rather than a number when fewer than three monthly returns are available, and they include every month — no hidden outlier filter that would let a real crash disappear from the metric meant to report it. The Money-Weighted Return (IRR) treats contributions as money paid in, discounted over the time it was actually invested
  • Capitale e mercato: one area (the capital that entered the portfolio — your net worth at the start of the period plus net contributions) under the net-worth line; the gap between the two is what the market produced, and hovering a month reads its net worth, invested base and market return. Contributions can legitimately go negative in a window where tracked spending outpaces tracked income, and the chart shows that honestly instead of hiding it
  • Rolling 12-month CAGR and Sharpe Ratio charts with 3-month moving average; always visible with an informative empty state when data is insufficient
  • Benchmark comparison: your portfolio against six model portfolios (60/40, All Weather, Buffett 90/10, Golden Butterfly, Permanent Portfolio, 100% ACWI), each annualised over the same months as your TWR up to its own last available month and always converted to EUR (monthly rates via Frankfurter API), ranked with the gap in points; the growth of 100 € against the 60/40 is drawn in the return tile
  • The hero return counts up on load; period changes settle without replaying every animation
  • Capitale investito: alongside the existing "Contributi netti" (external savings estimated from expense tracking), a companion figure shows what you actually bought minus sold through the operations register in the selected period — an info popover explains why the two numbers measure different things and will not match. Plusvalenze realizzate: a per-fiscal-year breakdown of realized gains/losses closed through the operations register, across all tracked investments
  • Dashboard KPI cards (Total Portfolio, Liquid Net Worth, Unrealized Gains, Taxes) animate their values on page load — numbers count up from zero once on mount; each card animates independently so the rest of the page stays stable during the animation
  • Dashboard hero trend chart has a period selector (3M / 6M / YTD / 1Y / 3Y / All — All is the whole snapshot history), a hover reading of each month's value on desktop, an all-time-high badge when your net worth reaches a new peak, and a "driven by" line showing which asset classes moved the most this month; a featured progress bar for your most relevant active goal appears alongside it when Goal-Based Investing is enabled
  • All major pages animate on load with staggered card entrances and smooth expand/collapse transitions; on the Panoramica the cascade plays the first time you open it in a session, and a later visit shows the tiles at once; respects system "Reduce Motion" preference
  • All charts animate on load: bars grow up from baseline, lines draw in left to right, area fills expand, ranked composition bars grow to width — covers every page with data visualization (History, Performance, Cashflow, Dividends, FIRE, Monte Carlo, Goals)
  • AI-powered analysis using Claude with Extended Thinking and web search
  • AI Assistant in the same shape as the rest of the app: it opens with a one-sentence verdict on the numbers the assistant will answer on — "Luglio è andato bene.", "Il 2026 finora va bene.", "Di luglio conosco solo il cashflow." — for the period chosen on the period axis beside it (Month, Year, YTD, Total history, or Libera for a free question, with the month or year picker right there); a free question with no period reads the Overview's own verdict on today's numbers. Under it the conversation is a tile — the suggested questions as rows to start, the thread as flat messages, the follow-ups as rows — beside a sticky companion of tiles showing the period's net worth with its change and where it started, its cashflow, and what the assistant knows about you, all before you ask; in Libera mode you can optionally attach any period as context. After each answer the assistant proposes follow-up questions you can send with one tap. It remembers goals, preferences, risk profile, and stable facts across conversations — shown in the "Cosa sa di te" tile and managed in a Memory panel of two tiles (goals with what each measures and its daily check, facts) — and surfaces a proactive "goal reached" tile when it detects from your data that a tracked target has been met. An "Allocazione vs target" suggested question triggers an instant allocation-vs-target comparison with purchase priorities. On the spending side it receives an exhaustive breakdown of the selected period — every category with every sub-category used, each with its share of total spending, alongside spending by type (fixed / variable / debt) and income by category — so "how much did I spend on utilities inside Home last year?" is answered from data rather than refused. It also sees your full category/sub-category taxonomy, including categories with no activity, so it can suggest where to file a new expense or whether a new category is warranted. Conversations and Memory open as windows in the app's one dialog shape — a bottom sheet on a phone, a centred panel above — and a saved conversation is deleted with two presses and no countdown, the row saying what goes («la conversazione e i suoi messaggi spariscono; la memoria resta»); responses stream as markdown (including tables) with full thread continuity. It also reads your Goal-Based Investing goals — assigned value, target, deadline, priority, suggested mix, and whether the current pace reaches the target, including the monthly contribution needed to close the gap and the value projected at the deadline (both labelled as projections, with the return they assume) — and when goal-driven allocation is on it reasons about the targets derived from those goals rather than the manual ones the app has stopped using. It can also propose a new goal: the reply renders as a card with the proposed name, target, deadline, priority and allocation, and nothing is saved until you press Conferma — the assistant never creates, edits or deletes a goal by itself. Web search for macro/geopolitical context (with a dedicated "searching the web" indicator and event citation) and behaviour preferences (response style, web context, automatic memory) in one place; a "Come funziona" disclosure below the conversation explains the periods, the web search and the memory. Runs on Claude Sonnet 5. Controlled rollout via feature flag.
  • Fully responsive on mobile and tablet: the period selector and the two actions sit under the verdict as full-width touch targets, tiles stack in one column, the heatmap reads on hover with a mouse and through its cells' titles on touch

Cashflow

  • Tracciamento answers before it lists — the tab opens with a one-sentence verdict on the period you picked ("Agosto sta andando bene." / "Agosto tiene, ma con poco margine." / "Ad agosto hai speso più di quanto è entrato."), followed by the facts in words — how much you set aside, income, spending, how spending moved against the same days of the previous month («sui primi 14 giorni di agosto») — and, whenever the period reaches past today, a second sentence that separates what has happened from what is only scheduled: the judgement is on the lived part («A settembre finora le spese superano le entrate di 355 €») and the calendar closes it («Con 1297 € di spese e 2456 € di entrate già in calendario da qui a fine mese, il mese chiude a +805 € (il 29%)»), both sides always named — and then a grid of tiles: the period's three figures with a six-month income-vs-spending chart and, while the month is running, where spending lands at the current pace (what you have spent so far, paced, plus what is already scheduled); spending and income by category with a reading of where the money concentrates and the residual row so the list adds up; your savings rate over the last twelve months with the average, the best and the worst closed month; and the transaction feed as the last tile, with its count and a reading that totals each kind of row it holds — so a search on a recurring note ("caffè") reads as a sum at a glance. On a phone or a tablet the filters count what is left of the period («2 filtri attivi: restano 27 movimenti su 112.», button «Mostra 27 movimenti»), and a movement's detail arms its delete in place, saying what the second press does to the account before you press it. One period picker, beside the verdict, drives everything — a closed month reads in the past tense, a year still running is compared with the same months of the previous year — while the transaction filters narrow only the list — with Divisione on, an "Intestatario" filter among them ("Tutti · In comune · Giuseppe · Marcella") and the owner's name as a chip on every attributed row, so "how much did Giuseppe spend this month" is one pick away and the list's own reading answers it; on a phone the feed's own filter bar repeats that same period picker, so the window can be changed from beside the list it slices. A period covers its whole calendar span: "Quest'anno" is January to December even in August, so instalments and recurring entries you have already created are counted and listed rather than hidden — and because they have not happened yet, the verdict closes by naming how much of the figure it just printed is only booked ahead ("Nel totale ci sono ancora 450 € di spese già in calendario da qui a fine anno"), each such row is tagged "In calendario" and loses its red/green colour, and the months not yet started are drawn lighter in the chart. "Da inizio anno" is the shortcut next to it for the window that runs from January to the end of the current month — so it carries what is already scheduled inside this month, and nothing of the months after it. With a mouse, the charts read the month under the pointer
  • Income, expense, and transfer tracking with custom categories and subcategories. Adding an entry starts with the type — Variable, Fixed, Debt/Installment, Income, or Transfer (move money between your own cash accounts) — picked from labelled cards, the same two-step flow as adding an investment in Patrimonio; the form that follows is titled and filtered for that choice, with advanced options (cost center, installments, recurrence) in a collapsible section. Recurrence is available on all three spending types — Fixed, Variable and Debt — and repeats monthly or yearly: a switch plus a Mensile | Annuale selector, a count that asks for months or years to match, and a line stating exactly how many entries will be created and the dates they will span before you save (up to 30 years of monthly payments, or 40 yearly ones). This is what lets a 17-year insurance premium or an annual subscription be projected forward the way a loan instalment always could. Income and transfers cannot recur — a transfer moves two accounts at once, and each occurrence would need its own pair of balance corrections. The whole series is created up front as real dated entries sharing a parent id, so it shows up in Cashflow and Analisi immediately and can be deleted in one action. Editing an existing entry skips the picker and keeps the type as a field inside the form, where the note explaining what a type change does to your balances lives. Transfers are net-zero: they debit the origin account and credit the destination, and are excluded from all income/expense/savings/budget and performance metrics. A linked account moves on each entry's own date: an entry dated today or earlier moves it when you save, one dated ahead waits for its day — and every instalment or occurrence of a series carries the account, so a mortgage entered in January for the whole year leaves the account one instalment at a time. A series recorded before this rule can be linked from any of its entries («Collega la serie a un conto»): the entries still to come take the account, the past stays as it is. Any entry can carry its bank fee («Commissione»): the fee becomes a spending entry of its own, in the category chosen in Settings → Spese, debited from the same account on the same date — edited from its entry and deleted with it; on a recurring series or an instalment plan every occurrence gets its own fee, so the «spese incasso rata» of a loan are two rows a month like the bank's statement. A transfer can land on a property, not only on an account: the deposit on a house you are buying leaves the account and becomes value of the house, net-zero, a patrimony changing form rather than a spending. A Debt entry can repay a loan («Riduce il debito di»): on the instalment's own date the loan's residual debt falls by the principal only — the instalment minus the month's interest at the TAN set on the loan (French amortisation), with today's split shown in the form before you save — and the repaid principal is stored on the entry, so editing or deleting it gives back exactly that; tick «Estinzione anticipata» and the whole amount is principal (a penalty goes in the fee); an existing mortgage series is linked from any of its entries («Collega la serie al mutuo»), the instalments still to come only. A cash account may go below zero, which is how a credit card is tracked: spending linked to it, and one transfer from the bank the day it is debited
  • The type of a saved entry can be changed across all five types, transfers included: the category re-attaches itself to the same-named one under the new type, the amount's sign and every linked account balance are corrected automatically (converting a transfer away reverses it on both accounts; converting an entry into a transfer asks for the destination account), and an inline note states what will change before you save. Bulk re-typing a whole category across the transfer boundary is refused instead — each row touches two accounts, so only the per-entry dialog can do it safely
  • Categories are identified by document, not by name, so the same name can live under two types (a "Casa" under Fixed and another under Variable) without their figures merging anywhere in the app — Analisi, Overview top-5, trend charts, cost centers, PDF export, periodic emails and the CSV importer all key by document. Where two do share a name, charts and lists spell out which is which — "Casa (Spese Fisse)" / "Casa (Spese Variabili)" — and leave unambiguous names alone
  • Analisi page (/dashboard/analisi): opens with a one-sentence verdict on where the money goes and what changed — "Nel 2026 spendi più dell'anno scorso." followed by the facts (the total, the change against the same window of last year, the heaviest category and its share, the category that moved the most, the categories out of scale this month) — over a grid of tiles: the period's three figures with year-over-year pacing and spending per month beside last year's; the categories out of scale this month; the largest single expenses; spending and income by category as the full, clickable lists; and the Sankey flow, sized to its widest column and opening the six largest categories into their subcategories on request. Every category and sub-category is a first-class object: click it anywhere — a category row, an anomaly, a top expense, a Sankey node, a comparison row, or the "Vai a categoria…" search (which also reaches entities with no spending in the period) — and its Scheda opens as a tile under the lists: the period total with its shares, a per-year table with signed year-over-year deltas (the current year compared like-for-like against the same months of last year) whose rows open on the change per sub-category, a 24-month trend with last year's dashed baseline, and the sub-categories or the underlying transactions. The focused entity lives in the page link and survives period switches. The four-state period selector (year to date / current year / past year + optional month / full history) sits beside the verdict — "Da inizio anno" stops at the end of the current month while "Anno corrente" covers January to December, so a purchase you split into instalments is visible in the year that holds it and the verdict says how much of that year has not happened yet; "Storico" runs from your history start year to the current year ("Dal 2025 al 2026"), never into the years an instalment plan reaches; the verdict and the Confronto compare each window against the matching one a year earlier — "Anno corrente" against the whole of last year (the reading says how many months are still only in the calendar), "Da inizio anno" against its same months, the month in progress against the same days of that month a year earlier ("su settembre 2025 (primi 14 giorni)" — and when those days hold nothing, the page says so instead of comparing); the Scheda's pace is measured on the months lived ("Media mensile · sui primi 9 mesi") and its reading names the window of every figure it prints; the year-over-year Confronto (selectable comparison year, a per-category delta ranking with "Nuova"/"Cessata" categories) and the long-term trends sit below the tiles behind two disclosures. All windows are declared next to their figures, and every chart respects the "history start year" preference from Settings. The Flusso can read by 50/30/20 role (opt-in in Impostazioni › Spese: give each spending category a role — Necessità, Desideri, Risparmi — and a subcategory can override it): it then opens on «Per ruolo» with «Per tipo» one tap away, what has no role yet reads «Da classificare», a period that spent more than it earned shows the gap as «Coperto dal patrimonio», and spending that is itself a saving is read as how much was set aside and how much was left over. On a phone the Flusso is a bar of the period's spending split by type, each type's categories as rows (a row opens its Scheda) and what was left over as a closing line, instead of a Sankey too narrow to read; the printed shares always add up to 100, and from 640px up the Sankey is unchanged
  • Budget tab: opens with a one-sentence verdict on the month — "Il budget di agosto tiene." / "Agosto rischia di sforare il tetto." / "Ad agosto hai superato il tetto." — followed by the facts in words (days left, share of the ceiling used, where the month lands at the current pace and by how much, and when the ceiling was or will be crossed: "Lo hai superato il 22", "superando il tetto il 29"), then a grid of tiles: the ceiling with a mark for "today" on its bar — what you have actually spent and, as a lighter second fill and its own figure, the rows already dated after today ("hai speso 656 € e hai altri 1297 € già in calendario") — the month-end figure, what is left per day (or, once over, by how much and since when, and your real daily pace against the ceiling's), the last six months each against the ceiling it had (recorded daily by the scheduled job); the categories at risk of going over by month end; the thresholds you actually crossed with what is booked so far, each read against the calendar of its own window ("soglia 50% · anno al 70%", amber only when you are ahead of it); your annual budgets read against the year; and every budget you set with its bar and month-end figure. Budgets are yours to create — per category, sub-category or type, monthly or annual (year-to-date), plus income targets — with an optional overall monthly ceiling on all spending; changes auto-save as you type, paused while the category budgets exceed the ceiling. A budget on a fixed category (rent, subscriptions, instalments) is never projected by the day. Threshold alerts (50/75/90/100%) also reach your emails. Fully responsive on mobile
  • Cost Centers tab (opt-in): group expenses by object or project (e.g. "Automobile Dacia"). The tab opens with a one-sentence verdict on your projects — "Automobile è il centro più caro." / "Automobile supererà il tetto di agosto." (the expenses already in the calendar carry it past) / "Casa al mare ha superato il tetto del 2026." — then a grid of tiles: the whole cost with this year, last year, the monthly average and the last twelve months as bars stacked by center; every center ranked by its cost with its count, last expense and where it stands against its ceiling; the centers idle for more than 90 days; and the archived ones below, out of the total. There is no period selector: a project's cost is its whole cost, and the few figures on another window say which ("quest'anno", "ultimi 12 mesi", "Tetto mensile · agosto" with a mark for today). A center's detail is the same shape on that center — verdict with the Modifica/Archivia/Elimina actions beside it, its cost with the ceiling's bar, what is spent this month and this year with what the calendar still adds ("con il calendario chiude a 1100 €" — never a pace: a project spends in blocks), the breakdown by category (when there is more than one) and by subcategory (tap one to read the cost net of it), its lifecycle dates, and every linked movement with the scheduled ones marked "in calendario". Optional per-center spending ceiling (monthly or annual, read exactly like the Budget page's, crossing day included), and archive/restore for finished centers — deleting one keeps its expenses, they only lose the tag. From a center's detail, "Collega spese…" links many expenses in one confirm — search, filter by category and year, tick (a recurring series or an instalment plan is one row that links every occurrence), with the reading naming what would move from another center before you confirm and an "Annulla" on the outcome — and each linked movement opens its expense or is unlinked in place. An open center lives in the URL (reload, the browser's Back and a shared link all work), a new center opens on a colour no active center wears and a worn swatch says whose it is
  • Divisione tab (opt-in): for a household that shares its expenses. It opens with a one-sentence verdict on the period — "Ad agosto le spese in comune sono 2410 €: 1446 € a Giuseppe (60%) e 964 € a Marcella (40%). A Giuseppe restano 754 € dei 2600 € di entrate; a Marcella restano 436 € dei 1600 €." — then a grid of tiles: the shared spending with its breakdown by category (only the shared rows, as the tile says), the split with the salaries it comes from, and one tile per person showing what their share of the common spending and their own personal spending leave of their income. That last figure is the point: it is what can go to a personal investment account at the end of the month. It counts only money that has actually moved: a bill dated later in the month is inside the shared total but not inside anyone's residual, and where the calendar takes each person is its own line underneath ("Con le spese ancora in calendario mancano 100 €") — so nobody is told their salary ran out over an instalment still sitting in their account. Every expense and income row can be marked as one person's with an "In comune · · " control in the entry dialog — "In comune" is the default, so a household whose spending is mostly shared only marks the exception, and every entry recorded before you turned the feature on already counts as shared. The shares are not a percentage you type: they come from the income of the period attributed to each person — salary, refund or gift alike, whatever its category. When one person has no income recorded yet, the page says so by name and shows no shares at all, rather than quietly charging the whole month to whoever did record one — and the tile that would have held the shares says what to do about it ("Registra le entrate di Marcella in Tracciamento e intestagliele"). Income nobody is named on is declared too ("Altri 1000 € di entrate sono in comune e non entrano nelle quote"), so a split computed on part of the month's income never reads like one computed on all of it. Income left "In comune" pays the shared spending first: a refund on a shared bill, a gift to the household or a salary nobody attributed comes off the pool before the shares are applied, the "In comune" tile shows it under its figure ("Entrate in comune −300 €", "Da dividere 2110 €") and the verdict says it in the same sentence ("le spese in comune sono 2410 €, meno 300 € di entrate in comune: 2110 € da dividere, 1266 € a Giuseppe (60%)…"); when the shared income covers the shared spending whole, the surplus is stated ("avanzano 100 €") rather than handed out. Income attributed to one person is theirs in full: it counts in their share and in what is left to them. The people are the same ones you list under Settings → Preferenze → Famiglia (where the pension RAL lives), so there is no second list of names to keep in step. On Tracciamento the same attribution shows as a chip on each personal row and as an "Intestatario" filter on the Movimenti list — and "Attribuisci spese", in the page header, opens exactly that list filtered to the shared rows. The monthly summary email carries the same figures as a "Spese in comune" section — and since the periodic emails already take more than one recipient, both people can receive it. There is deliberately no "who owes whom" settlement: the question this tab answers is how much is left to each of you
  • Bulk move transactions between categories/subcategories (cross-type supported)
  • Deleting from the Tabella view arms in the row — «Conferma» beside a sentence that says what comes back («Eliminando, il conto viene riaccreditato di 373,81 €») — and a row of an instalment plan or a recurring series asks the one question a series adds: solo questa o tutte le rate?
  • CSV export
  • CSV import (Settings → Spese): migrate historical income/expense data from a CSV file (Italian or English headers, ;/,/tab delimiter, IT or EN number/date formats). Categories resolve by (name, type) — same-named categories of different types import side by side, a row without a type inherits the single existing namesake's type, and duplicates sharing both name and type attach to the oldest one with a note in the preview. Shows a full preview — valid rows, discarded rows with a reason, categories/subcategories that will be created, disclosures — before anything is written, and the whole import can be undone in one tap. Transfers aren't supported (a transfer needs origin/destination accounts a historical row can't provide) and account balances are never touched by the import

Dividends

  • Dividendi answers before it lists — the tab opens with a one-sentence verdict on your dividend income ("Il flusso di dividendi cresce." / "…tiene." / "…è in calo."), followed by the facts in words — what you cashed in the period, how it moved against the comparable window before it, from how many instruments, what the portfolio yields on your cost, and which payment comes next — and then a grid of tiles: net income with the month-by-month shape and the next payments; how reliable the income is (how many months actually paid, and how much of it hangs on one payer); yield on cost against yield on today's price; who pays you most; what each year brought; and the payments themselves, as a table or a calendar, as the last tile. One period selector (Mese / Anno / 12 mesi / Storico), beside the verdict, drives everything; the instrument and type filters narrow only the payments list. A dividend or coupon can credit a cash account when its income is recorded: each instrument names its own account in the asset form (two brokers, two accounts), Impostazioni › Dividendi holds the default, and only a payment dated today or later moves a balance — payments already in the past are recorded without touching it, so downloading an instrument's history never counts them twice
  • Received and announced are never one figure. A payment with a future date is a promise, not income: the table prints two totals instead of one, the calendar greys it and says how much of the month is cashed and how much is still expected, and every sentence counts the two apart — on a phone too, where the row carries an "Attesa" chip and the calendar's day says "attesa". Announced payments are bounded by the period too — a coupon due later this month belongs to "Mese", one due in December to "Anno", a final premium years away only to "Storico"
  • Two populations, both named. The verdict and the payments list read the whole registry — a dividend from a stock you have since sold was real income. "Affidabilità" and "Chi paga di più" answer in the present tense, so they measure only what you still hold and say what the sold instruments paid ("altri 186 € da 2 strumenti venduti"), never ranking a payer that is no longer yours
  • Recording by hand: the form lists every equity and bond, held or sold, proposes the withholding at the instrument's own tax rate (12,5% on a BTP, 26% on a stock) and "Cedola" as the type of a bond's payment; a refused submit says which fields are missing, in Italian, and takes you to the first one
  • Multi-currency dividend recording with automatic EUR conversion
  • Borsa Italiana scraping for Italian market data (dividends and bond prices)
  • Monthly calendar view; clicking a day opens that day's payments with its own received/expected subtotals. Month navigation is bounded by the selected period — with "Mese" the arrows are gone, since the period selector is that control
  • Yield: yield on cost gross and net, against the yield on today's market price, plus median year-over-year DPS growth. Every figure here is measured on the trailing twelve months of what you currently hold, whatever period is selected — the tile says so rather than appearing to follow the axis
  • Total Return per Asset and Dividend Per Share Growth live in a "Dettaglio" section under the grid. Total Return combines capital gain % and all-time net dividends received % (calculated at historical cost basis per payment, not diluted by later purchases) to show the true investment return per asset; for investments tracked in the operations register the capital-gain figure comes from your real buy/sell history — including fully-sold positions, shown with a "Chiusa" badge, and partial sells. DPS Growth is the year-by-year gross DPS history per equity asset with YoY% and CAGR columns, YoY and CAGR stopping at the last closed year; tap any asset on mobile for a vertical year-by-year dialog

Historical Analysis

  • Automatic monthly portfolio snapshots (via Vercel cron)
  • Storico answers before it charts — the page opens with a one-sentence verdict on your whole history ("Il patrimonio è cresciuto, e sta accelerando." / "…cresce al ritmo di sempre." / "…è cresciuto, ma ha rallentato." / "…ma nell'ultimo anno ha perso."), followed by the facts in words: how much your wealth grew since the first snapshot and at what yearly rate — contributions included, and the sentence says so, because this is wealth growth and not the investment return Rendimenti measures — your best month, your last doubling, and how the last twelve months compare with your lifetime average. Then a grid of tiles, each with its own one-line reading
  • Evoluzione: today's value, three chips (growth since the first snapshot, the wealth CAGR named as "versamenti inclusi", the last twelve months) and the net-worth series with your notes as markers on the line — hover a month for its value, its change on the month before and its note; colors theme-aware across the six color themes
  • Raddoppi — doubling time analysis with geometric calculations and fixed milestone thresholds: the completed doublings as rows, the one in progress with its track, and the next one projected at the same pace the verdict judges you by (the average monthly increase of the last twelve months, in euro, linear on purpose); the mode toggle sits in the tile's corner
  • Composizione: one tile answering how your wealth is split, on two cuts of the same euro — Asset class or Liquid vs illiquid — chosen with a single toggle. A 100%-stacked area shows how the mix drifted month by month (band thickness is the share, so the flat top edge is a permanent check that the parts add up), with a ranked breakdown underneath giving each class its value in euro, its share, and how that share moved against the same month a year earlier. Hovering a month opens that month's balance sheet. Anything the monthly snapshots cannot attribute to a class is labelled "Non attribuito" rather than quietly dropped. Each snapshot also records how your pension funds were split across asset classes that month, so the Previdenza band is separated from Azioni/Obbligazioni using the split that was true at the time; months recorded before this fall back to your fund's current split, and the note on the tile says from which month the figure is measured rather than estimated
  • Driver della crescita: for every year since your cashflow is complete, where the year's growth came from — what you saved (income minus spending already happened), what the market did (measured instrument by instrument, the quotes you traded counted from their trade price, the same figure as the Panoramica), the tax withheld on your sales, the mortgage principal repaid, what went into your pension funds, and what none of these explains («altre variazioni») — as a ledger behind each year: the parts are rows that add up, to the euro, to the growth they close on, so you read the arithmetic instead of trusting a sentence (the sentence names the two engines, the heavier first, and nets the rest into one figure); «Dal {anno}» sums every year as one more row; then the last twelve months as bars, every part in the hover; a year in which the market took is said in words, never as a share
  • Valore per strumento: pick any recorded month to see each holding's value that month (read straight from the saved snapshot, so currency/real-estate/price rules are already applied), its share, and its change on the previous month split into a price effect (market movement) and a quantity effect (your buys, sells and deposits); a change moved mostly by quantities is a flow, so it carries no gain/loss colour (a 38.000 € sale is not a loss); sort by value or by any of the three changes; tick instruments to total them and chart that subset's combined value across every month on record — the table has the whole tile until you tick something, then the selection takes its place beside it and follows you down the list; from the keyboard the instruments are one Tab stop, the arrows move between them
  • A "Dettaglio" section under the tiles keeps the year-over-year variation, savings vs market month by month over the whole history, the Labor & Investments recap (earned from work, saved from work, other income, then the Driver's own market, sale taxes, mortgage, pension contributions and other changes, adding up to the growth, plus the monthly chart — with a setup prompt linking to Settings when labor categories are not yet configured) and your notes

FIRE Planning

  • FIRE calculator as a verdict over tiles — the tab opens with one rule-generated sentence that answers "when?": "FIRE nel 2032, a 44 anni." followed by the facts in words (the gap to the FIRE number, the monthly pace, the year and the age, and the passive income the plan lands on in both moneys — "2300 € al mese di oggi, 2667 € del 2032 con l'inflazione al 2,5%" — plus the pension capital locked and its unlock year when the bridge model is on; "Sei già FIRE.", "FIRE oltre i 50 anni." and, when an input is missing, "Numero FIRE non calcolabile." / "Nessun patrimonio FIRE." are the other verdicts; in those two the four tiles stay, each saying why it cannot answer, and the Traguardo carries the one way out — "Registra le spese nel Cashflow" or "Aggiungi il primo asset"). A target already cleared today reads "oggi · già raggiunto" in every scenario, never "tra 1 anno". Under it a 12-column grid: Traguardo (the FIRE number as the hero figure, the "% verso FI" chip — "% del numero FIRE" once you are past it — over a 3px track, and the projection filling the tile in one of two views switched by the tile's own Scenari | Ventaglio toggle), Base di calcolo (the net worth the page runs on, the expenses and savings of the cashflow year it names, the SWR, then the two rows that make the number honest — the state pensions saved in Coast FIRE, net and dated by your age ("13.000 € · netti l'anno, dal 2060"), and the tax on withdrawals read from your instruments' cost basis ("26% · sul 45% di plusvalenza latente oggi") — each either inside the number or declared out with its reason ("non considerate: manca l'età", "non stimate: nessun PMC in euro") — and the pension-lock switch, which saves on change), Reddito passivo (annual / monthly / daily at the SWR, the years of expenses covered with the liquid and illiquid split, the current withdrawal rate against the safe one) and Scenari (Orso · Base · Toro as rows with their parameters and their FIRE year). The FIRE number, the verdict and the chart share ONE expense figure: the last full year's, or the running year annualized and said so — and ONE requirement per year: expenses less the pensions from their start, grossed up for the tax the withdrawals pay, the locked fund at its unlock ("27.600 € di spese ÷ SWR del 4%, meno la pensione dal 2060, tasse sui prelievi comprese"); the Scenari chart's dashed line is that requirement, and the verdict closes on it ("…copre le tue spese, tasse sui prelievi comprese; dal 2060 la pensione statale ne copre 13.000 € l'anno")
  • Settings (withdrawal rate, primary residence, INPS age and the RITA hypothesis) and the three scenarios' growth and inflation live below the grid in a "Parametri" disclosure that opens by itself only until an SWR is saved, or when an edit is unsaved; the historical FIRE runway, the cashflow-vs-passive-income history and the explainer sit in a "Dettaglio" disclosure
  • Projection with three views, switched inside the Traguardo tile: Scenari is the deterministic Bear/Base/Bull chart (three portfolio series plus a single dashed base-scenario FIRE target — Bear/Bull targets in the tooltip, which also names the pension-unlock step); Ventaglio is a Monte Carlo fan of the accumulation phase — 1,000 simulated paths with 10–90 and 25–75 percentile bands, the median, ~40 sample paths and the moving FIRE target — and the tile's footer states the cumulative probability of reaching FIRE by the projected year; Distribuzione is the FIRE year across those same paths — a histogram by calendar year with the base scenario's bin outlined and one grey bar for the paths that never get there, the year by which one path in ten, half and nine in ten are FIRE ("Metà dei percorsi è FIRE entro il 2034, come nel base; un percorso su dieci entro il 2030, nove su dieci entro il 2041; 37 su 1000 non ci arrivano entro il 2066"), then two sentences that turn the tail into decisions: the lever ("Perché anche nove percorsi su dieci siano FIRE entro il 2034 servirebbero 6000 € l'anno di risparmio in più (500 € al mese); il 10% più fortunato passerebbe dal 2030 al 2029" — found by re-running the same thousand paths on the same market draws, so the comparison is between plans and not between dice) and what happens after ("Prelevando le spese dal proprio anno FIRE, il capitale dura fino al 2076 (a 90 anni) in 940 percorsi su 963; nel 10% peggiore si esaurisce entro il 2068"). The paths are drawn with a fixed seed, so two openings show the same distribution. Market returns and volatility are derived from your real portfolio allocation
  • Coast FIRE tab — the same verdict-over-tiles shape as the calculator, on one question: "can I stop contributing?". It opens with a one-sentence verdict — "Non ancora: continua a versare." / "Sì, puoi smettere di versare." — followed by the facts in words: how much is missing to today's Coast FIRE number, what your current patrimonio becomes at the target age with no further contributions against what is required there, the year your current savings pace (the calculator's) crosses the Coast number of that year ("Al ritmo attuale, 12.000 € l'anno di risparmio, lo raggiungi nel 2031, a 40 anni") and, with the bridge model on, the pension fund that stays locked and counts in neither figure. Then three tiles: the target (the shortfall as the headline figure, the progress chip and track, the liquid-only read, and the Bear/Base/Bull projection with the required capital as a dashed line that steps when the fund re-enters, plus a dotted line for the base scenario fed by your savings until that year and coasting from there — the tooltip names both the step and the year); the inflows the calculation already discounts (your pension fund's unlock year and each state pension's start date and net real amount, every figure at today's value); and the three scenarios as rows, each with its own Coast number and the two rates its real return comes from. Without a positive patrimonio, expenses or an age, the three tiles stay and the target tile carries the one way out. Below, an "Ipotesi" disclosure holds the settings as four tiles with one save (ages, custom expenses, state pensions, IRPEF brackets, the model in four steps — open by itself until an age is saved), and a "Dettaglio" disclosure the coverage phases, the target-age need beside the steady state, the per-pension impact from gross nominal to net real, and how to read it. All chart colors theme-aware
  • Coast FIRE supports one or more state pensions with editable IRPEF brackets, exact pension start dates, scenario-specific real net conversion, a guided summary that separates target-age need, bridge years, and post-pension steady state
  • Multi-scenario projections (Bear / Base / Bull) with inflation adjustment
  • Per-scenario FIRE numbers with automatic savings stop at FIRE reached
  • Historical FIRE runway view with rolling 12-month expenses and separate total/liquid deltas
  • What If Analysis tab — stress-test your plan against life events (job loss, major purchase, savings/spending change, windfall). It opens with a one-sentence verdict whose tone follows the delta in years ("Il FIRE slitta di 1 anno.") and names what the event does to your patrimonio, your FIRE number, your FIRE year, your passive income and your Coast FIRE plan; then four tiles — the two projections drawn over each other (today's plan as a neutral baseline, with both capitals read at today's FIRE year), every figure as a before → after row with its signed change, the event form (which income sources stop, and how the hit splits between forgone savings and expenses drawn from the portfolio), and the sensitivity matrix of years-to-FIRE by expenses × savings on today's plan. The pension bridge of the calculator applies here too, so "before" agrees with the calculator's year.
  • Goal-Based Investing (FIRE › Obiettivi): allocate portfolio portions to financial goals (house, retirement, emergency fund, etc.). The tab opens with a one-sentence verdict on whether you are on track — "Sei in rotta su ogni obiettivo." / "Un obiettivo su tre è in ritardo." — followed by a clause per goal (the extra monthly pace a late goal needs by its deadline, the deadline of the ones on track, the arrival of the undated, the reached). Then five tiles: every goal as a row in urgency order with its progress, what is missing, the deadline, the priority and an "In rotta / In ritardo / Raggiunto" tag (tap a row to select it); the selected goal's trajectory — the value it reaches at its deadline, the pace paid and the pace required, the months left, the expected return inferred from its recommended asset mix, and the glide-path chart — with edit and delete beside it; the milestones (the order goals will be reached at today's pace, a late goal dated at its real arrival with the months past its deadline); the allocation the goals derive beside the one your quotas hold (with Goal-Driven Allocation on); and the quotas grouped by goal beside the instruments that still have free value, closed by the unassigned total. A "Dettaglio" disclosure splits a new deposit across under-funded goals by gap × priority and explains the calculation. The AI Assistant can discuss these goals and propose new ones, which are created only on your explicit confirmation
  • Goal-Driven Allocation: optionally derive portfolio allocation targets as a weighted average of goal recommended allocations, with automatic fallback to manual targets
  • Fully responsive on mobile and tablet — tab navigation uses a dropdown on small screens

Monte Carlo Simulations

  • Opens with a one-sentence verdict on how likely the plan is to hold — "Il piano regge nell'84,2% dei casi." (green from 90%, amber from 80%, red below) — followed by the facts in words: the share of simulations in which the capital lasts to your age at the end of the horizon, what the median case closes with, the year the worst tenth runs out, how the bear and bull scenarios fare, and the pension fund that re-enters at its unlock when the bridge model is on
  • One run is the three scenarios (Bear / Base / Bull) on the same plan, and the base scenario is what the verdict reads. The simulation runs by itself when the tab opens — on your portfolio's allocation normalized to the four classes (crypto and cash excluded), your net worth net of any locked pension fund and your planned annual expenses — and afterwards on "Esegui simulazione"; until you press it, the Parametri tile says the figures still belong to the last run
  • Four tiles: the probability as the hero figure with the failures, the median failure year and the survivors, over the fan of the base scenario (10–90 and 25–75 bands, the median line, the capital exhausted as a dashed line); the final values as three percentiles and a ten-bin histogram, the median's bin outlined and the last bin taking the tail past the 95th percentile — and, switched inside the same tile, "Esaurimento": the simulations that fail by the year their capital runs out ("Le 1579 simulazioni che falliscono esauriscono il capitale tra il 2041 e il 2060, la metà entro il 2052"), which used to be a 0 € in the first bin; the three scenarios as rows, each with its probability, its median final value and the year its worst tenth runs out; and the plan itself — starting capital with "Totale / Liquido" shortcuts, horizon, withdrawal, simulation count, the four-class allocation and the three scenarios' returns, volatilities and inflation, saved to Firestore per user; under the withdrawal, what it is net of and what it pays: the state pensions from their year ("Pensione statale: −13.000 € l'anno tolti dal prelievo dall'anno 34 (2060), netti, al valore di oggi") and the tax on each sale ("ogni prelievo vende quanto serve a pagare il 26% sulla plusvalenza (40% del capitale oggi)"), or why neither is in
  • A "Dettaglio" disclosure below the tiles holds the three medians drawn over each other, the base scenario's percentiles every five years and an explainer of the method and its limits
  • All chart colours and tooltips theme-aware across the six colour themes; fully responsive — at 390px the tiles stack with the plan last and the percentile table scrolls inside its tile

Other

  • Navigation shell — A quiet frame built so the page's verdict is the first thing you read: a one-line page header (section · title · date), a desktop sidebar with three route groups under small uppercase labels, the AI assistant as a plain route, and an icon rail with 44px targets when collapsed; on the phone a floating bottom pill with the three primary routes and an "Altro" drawer listing Analisi and Pianificazione; in landscape on tablets a top bar opens the same navigation as a sheet; since 2026-09-12 a click on any of those links runs the change of page as one scene (a native view transition: the header line and the verdict of the page you leave become those of the page you open, in place, while the shell — the phone's bottom pill included — stays still — Firefox keeps a plain fade, reduced motion skips the scene); since 2026-09-28 the frame is part of the page the server sends, so the sidebar, the bottom pill, a muted header line and skeleton tiles are on screen before the sign-in check has run (no spinner), the profile shows two muted lines until the account is known, and a saved colour theme is on the page from the first paint
  • Loading, empty and error states — When the app cannot answer, it says which of three things happened: nothing is recorded yet, or something is and it is zero, or your data could not be read. Each reads differently — an absence is one sentence in the tile's own reading line with at most one action, a measured zero keeps its figure, and a failed read is an alert naming what was not read and what was not touched. A dropped connection therefore never shows you zeros, and a page never loads forever waiting on a request that already failed. Loading placeholders stop pulsing for anyone whose system asks for less motion, and toasts carry their severity on the icon and a thin rule rather than in the panel's colour.
  • The last known figures first — Since 2026-09-29 the app keeps its last successful reads on the device (instruments, snapshots, movements, categories, settings, pension contributions, the trade ledger, cost centres, goals, the Hall of Fame and the Panoramica summary — never the demo account's), so a reload or a return after an hour opens on the figures you saw last time instead of a skeleton, and rereads them at once behind. While that reread is in flight the page header says so in its own line — "Aggiornato alle 18:42, sto rileggendo…", "Aggiornato ieri alle 18:42…" — and the sentence goes the moment the fresh figures land, without anything moving; a save you make shows its result the usual way, with no sentence, because nothing on screen is old then. What is kept expires after a day, is forgotten at sign-out, and can be switched off with NEXT_PUBLIC_PERSIST_QUERIES=false.
  • Dialogs and forms — Every dialog in the app shares one shape, the same one the pages use: a small uppercase line saying where you are and how far ("NUOVA VOCE · PASSO 1 DI 2", "REGISTRO OPERAZIONI · VWCE"), a title naming the act rather than the form, and one sentence under it that answers the question you are about to act on — what the form wants, what is happening while it saves, and why it did not work when it fails, in the same place, without moving the title. Failures are written in the app's own words ("Non hai i permessi per scrivere su questo account.") rather than relayed from the database in English, and a cause the app does not recognise gets a sentence that claims nothing instead of a log line. On a phone every dialog is a bottom sheet with the main action as the top row at a 44px full-width target; on a desktop it is a centred panel at one of four widths chosen by what it holds. Destructive actions name what you are about to lose and take two presses to mean it — "Elimina 47 movimenti" → "Premi di nuovo per elimina 47 movimenti" — with no countdown, and Esc cancels the confirmation rather than the dialog
  • Periodic email summaries with AI commentary — Automatic portfolio recap emails sent at the end of each month, quarter (March/June/September/December), half-year (June 30 / December 31), and year (December 31), each with its own toggle. Emails include net worth change vs the previous period, asset class breakdown with allocation %, best/worst performing asset class (by Δ% and Δ€), income vs expenses with savings rate, full income and expense category breakdowns, top 5 individual expense transactions, dividends received, and a Rispetto a un anno fa tile comparing net worth, income, expenses and savings against the same period one year earlier (net worth from end-of-period snapshots, cashflow from period totals) — absent on the yearly summary, where that comparison is the one the rest of the email already makes. Every email opens on a rule-written verdict — "Agosto è cresciuto: il mercato ha spinto." with the facts under it — which is also the inbox preview line, so the message answers its question before you open it; the AI commentary sits second, because its generation can fail and an opening that can vanish is not an opening. Each block is a tile: a label, the window it covers, one line of reading, then the figures. The AI-generated narrative is structured in six sections (overview, patrimony and investments, vs previous period, vs same period last year, income/expense changes with likely causes, takeaways) and reads the same complete data the in-app Assistant does: your full category → subcategory spending tree, income by category, current allocation with targets and gaps, your investment goals, plus the budget alerts of the month and an already-calculated split between what you saved and what the market moved. Its length scales with the period (500 words monthly, 700 quarterly and half-year, 900 yearly). Web search for macro context follows your Assistant preference — enable "contesto macro" if you want market events cited. Recipients shared across all email types; manual send buttons in Settings for on-demand previews. A separate opt-in budget email is sent every Sunday with the status of all your monthly and annual budgets plus a one-line AI summary; any category that has actually gone over its limit also lists the individual expenses behind the overrun (date, subcategory, note, amount). Nothing in it is weekly — it arrives on Sunday, but the ceiling and the monthly budgets are month-to-date, the annual ones year-to-date, and the projections land at month end, so each block states its own window. Every budget carries the same 3px track the app draws, with today's mark on the budget's own window, and income targets have their own block: reaching one is good, and it does not belong among the things you can overspend. Powered by Resend (free tier sufficient for personal use)
  • Public landing — The Panoramica, told to someone who has no data yet. It opens with the sentence the app is about ("Il tuo patrimonio, spiegato prima che misurato." — the same one the sign-in page makes), two actions, and then the dashboard's real tiles: the net-worth hero with its variation chips, market digest and period selector, the month's cashflow, the asset-class composition and the goals — running on a made-up profile that is declared as such right above the grid, and whose numbers add up (the classes sum to the total, the chips are computed from the chart, the month-end projection is the app's own rule; all of it pinned by tests). Three more tiles show no figures at all: they say what Rendimenti, FIRE and Previdenza actually compute, and the few numbers they do carry — six benchmark portfolios, ten thousand simulated paths per scenario, the current pension-deduction ceiling, the count of asset classes — are read from the code that implements them, so the page cannot claim something the app stopped doing
  • Public demo mode — "Prova la demo" button on the landing and login pages auto-logs visitors into a shared read-only account. All mutation actions are disabled; the AI Assistant is fully blocked; inside the app an amber strip says so at every width. Set NEXT_PUBLIC_DEMO_* env vars to enable; leave them empty and both the button and the sentence promising it disappear, on a self-hosted deploy
  • Color Themes — Six selectable color themes (Default, Solar Dusk, Elegant Luxury, Midnight Bloom, Cyberpunk, Retro Arcade) with per-user persistence in Firestore and localStorage. Theme selector in Settings → Aspetto with light/dark preview swatches. Switching dark/light mode plays a circle-reveal animation from the toggle across the whole page. Charts update their palette to match the active theme
  • Dark mode — Full dark/light/system theme support. The header toggle cycles through three states: Light, Dark, and System (follows OS preference), using Sun, Moon, and Monitor icons. The same toggle is available on the public landing, login, and registration pages, so visitors can pick their mode before signing in. Every page, chart tooltip, and UI component is properly themed
  • Authentication flow — Login and registration take the dashboard's own shape, scaled to what they are: one column with the sentence the app is about ("Il tuo patrimonio, spiegato prima che misurato.") and one card. The card's first line is also its status line — what the form wants, then what it is doing, then how it went — so there is no second feedback paragraph under the button. Errors are written in Italian rather than relayed from Firebase ("Email o password non corretti." instead of "Firebase: Error (auth/invalid-credential)."), and a cause the app does not recognise gets a plain sentence instead of the English one. On Register, the sentence states the access rule before you type — including "La registrazione è riservata alle email autorizzate" when the invite list is on, and a closed-registration page that offers the sign-in link instead of a form — and the two password rules ("Almeno 6 caratteri", "Le due password coincidono") are rows that tick as you type. Standard 36px fields with the app's focus ring, a "Mostra / Nascondi" word instead of an eye icon, 44px targets on phones, and autoComplete hints for browsers and password managers
  • Hall of Fame — Your personal records, opened by a one-sentence verdict on the best month you ever had and where the running month and year stand among them. No period selector: a record is a position, not a period. Five tiles — the best months for net worth (with the twelve record months drawn in the order they happened, and the worst month underneath), the months with the most income, the months you kept the most of it (income minus expenses, only where income was recorded), the years, and the notes you filed on your own records. The full ranking — twenty months or ten years, for growth, decline, income, spending or saving — sits below behind a «Dettaglio» disclosure with its own switcher; a note is filed from the row it explains (period and ranking already written) and survives the ranking it was written for; the worst month closes on the recovery since («da allora 32 mesi su 35 in crescita»), and the header dates the last recalculation
  • PDF Export — 7 configurable sections with custom year/month period selection; sections auto-disabled for past periods when historical data is unavailable. Page one is the report's verdict, not a title page: one sentence on how the patrimony moved, what it is worth, over how many instruments, and how much of the change was money you put in — with your name, the date and the sections included at the foot. Each section then reads like a page of the app (the question, the window, one line, then the figures), the titles are Italian throughout, and colour means what it means on screen — a gain, a loss, an asset class — rather than a brand accent. The Cashflow section states where its window starts: a "Totale" export begins at the year you marked as reliable, and saying so is what stops those missing years reading as years without spending
  • Settings — Every group of settings is a tile that says, in one line, what it is currently doing and what that changes elsewhere ("Base gestita: fondi pensione e asset esclusi restano fuori", "Bollo allo 0,2% attivo", "Marcella vede e modifica tutto — spese, asset, dividendi — con le sue credenziali"), with the controls under it; when something is not set, the line says what stops working instead of showing a blank. This is the one page with no verdict sentence: a settings page does not measure anything. Six tabs — Preferenze (the calculation basis of Rendimenti, portfolio costs, the FIRE toggles, household members for the pension tax benefit, periodic emails), Allocazione (the plan's total with its target leverage, the Azioni/Obbligazioni formula with the age and risk-free rate it reads, per-class targets with sub-categories inline), Spese (default accounts, CSV import, categories), Dividendi, Condivisione and Aspetto (light/dark/system beside the six colour themes). One Salva for the whole page validates everything, and a target group that does not add up is shown on its row and reached by Salva itself; while any tab holds unsaved edits, the tab carries a dot and a bar at the bottom names the tabs, with «Annulla modifiche» beside «Salva». What saves itself — the colour theme, the light/dark mode — says so, and a list that could not be read (the shared accesses, the categories, the accounts) says it failed instead of showing an empty one. Two tiles show settings owned by another page (the FIRE plan's parameters, the Assistant's preferences): they state the current values and link to where those are edited, so no setting has two places that can save it. Mobile tab navigation uses a segmented pill control; on desktop the section tabs sit under the one-line page header Since 2026-10-08 each tab is a view of one shared draft: a value typed in one tab is still there after a visit to another, typing re-renders that tab alone, a collapsed group of sub-categories is not on the page, and a reload opens on the saved form at once with "Aggiornato alle…" in the header while the fresh read lands behind.

Quick Start

# Clone the repository
git clone https://github.com/GiuseppeDM98/net-worth-tracker.git
cd net-worth-tracker

# Install dependencies
npm install

# Copy and configure environment variables
cp .env.local.example .env.local
# Edit .env.local with your Firebase credentials (see Prerequisites below)

# Start development server
npm run dev
# → http://localhost:3000

For the full setup guide including Firebase configuration and Firestore security rules, see SETUP.md.

Prerequisites

  • Node.js 24.x (engines.node in package.json, the same major on Vercel)
  • Firebase project with Firestore + Authentication enabled (free tier is sufficient)
  • Vercel account (recommended for deployment and cron jobs) or Docker for self-hosting
  • Anthropic API key (optional — enables AI performance analysis)

Environment Variables

Copy .env.local.example to .env.local and fill in your values:

Variable Required Description
NEXT_PUBLIC_FIREBASE_* (6 vars) Yes Firebase client SDK configuration
FIREBASE_ADMIN_* or FIREBASE_SERVICE_ACCOUNT_KEY Yes Firebase Admin SDK (server-side)
CRON_SECRET Yes Secret for authenticating cron job requests
NEXT_PUBLIC_APP_URL Yes Your deployed application URL
NEXT_PUBLIC_REGISTRATIONS_ENABLED No Toggle new user registration (default: true)
NEXT_PUBLIC_REGISTRATION_WHITELIST_ENABLED No Enable email whitelist for registration
NEXT_PUBLIC_ENABLE_TEST_SNAPSHOTS No Enable test snapshot generation in Settings
NEXT_PUBLIC_PERSIST_QUERIES No false switches off the on-device cache of the last successful reads (pages then start from a skeleton on every load); unset keeps it on
ANTHROPIC_API_KEY No Enables AI-powered performance analysis
FRED_API_KEY No Lets the daily cron refresh the ECB deposit facility rate history (via FRED) served by /api/benchmarks/ecb-rates; the Rendimenti page itself uses the risk-free rate configured in Settings
RESEND_API_KEY No Enables automatic monthly portfolio summary emails (via Resend)
RESEND_FROM_EMAIL No Sender address for monthly emails (e.g. onboarding@resend.dev for personal use)
NEXT_PUBLIC_DEMO_USER_ID No Firebase UID of the shared demo account
NEXT_PUBLIC_DEMO_EMAIL No Email for demo auto-login (shown on landing page)
NEXT_PUBLIC_DEMO_PASSWORD No Password for demo auto-login

See .env.local.example for detailed comments on each variable.

Security Notes

  • NEXT_PUBLIC_FIREBASE_* values are client configuration, not server secrets. They are expected to be visible in the browser bundle.
  • Keep FIREBASE_ADMIN_*, FIREBASE_SERVICE_ACCOUNT_KEY, CRON_SECRET, and ANTHROPIC_API_KEY server-only.
  • Private App Router API routes are expected to verify Firebase ID tokens server-side. Scheduled cron flows authenticate separately with CRON_SECRET.

Architecture

┌─────────────────────────────────────┐
│          Next.js App Router         │
│  (SSR pages + API routes + cron)    │
├──────────┬──────────┬───────────────┤
│  React   │  React   │   API Routes  │
│  Pages   │  Query   │  (server-side)│
├──────────┴──────────┴───────────────┤
│           Service Layer             │
│  (Firestore, Yahoo Finance, AI,    │
│   scraping, metrics, PDF)           │
├─────────────────────────────────────┤
│  Firebase Auth  │  Firestore DB     │
└─────────────────┴───────────────────┘
         External APIs:
   Yahoo Finance · Frankfurter · Borsa Italiana · Anthropic · FRED

Key design patterns:

  • App Router with protected dashboard routes
  • Service layer (lib/services/) for all business logic
  • React Query for client-side data caching and mutations
  • Feature-based component organization (by domain, not by layer)
  • Shared layout system (PageContainer, PageHeader, PageTabBar) for consistent page structure
  • Timezone-aware date handling (Europe/Rome)

Documentation

For contributors and AI agents, the repo keeps its guidance split by scope:

File Scope
CLAUDE.md Index — what each feature is and where it lives
AGENTS.md Repo-wide conventions, data/state patterns, UI patterns, testing, workflow
doc/guide/ One file per domain — a page, a tab, a subsystem (stati, dialog, temi, account-condiviso-demo, shell, cache-persistita, e2e-emulatori for the Playwright and emulator harness) — the full rules and blind spots for that area
perf/ · doc/guide/velocita.md The size budget (budget.json), the benchmark's routes and last runs; the manual — commands, baseline, raised ceilings, census — is the guide
DESIGN.md The aesthetic spec (normative)
PRODUCT.md · SETUP.md · WORKFLOW.md Positioning · environment/emulators · session rules

Tech Stack

Category Technology Purpose
Framework Next.js 16, React 19 + React Compiler SSR, routing, API routes; automatic memoization
Language TypeScript 5 Type safety
Styling Tailwind CSS v4, shadcn/ui UI components and design system
Data React Query (TanStack) Client-side caching and server state
Backend Firebase (Firestore + Auth) Database and authentication
Animation framer-motion Page transitions and micro-interactions
Charts Recharts, @nivo/sankey Data visualization
Finance yahoo-finance2 Real-time price data
AI @anthropic-ai/sdk Performance analysis
PDF @react-pdf/renderer Export reports
Forms react-hook-form, zod Form handling and validation
Dates date-fns, date-fns-tz Timezone-aware date operations
Scraping cheerio Borsa Italiana dividend and bond price data
Testing Vitest · Playwright Unit testing (4564 tests) · browser E2E against the Firebase emulator (40 specs)

Development

Commands

npm run dev        # Start dev server with hot-reload
npm run build      # Production build
npm run start      # Start production server
npm run lint       # Run ESLint
npx knip           # Find unused files, exports, and dependencies (see knip.json)
npm test           # Run unit tests (single run)
npm run test:watch # Run tests in watch mode

# Local Firebase Emulator Suite (offline dev/testing — never touches production; requires a JDK)
npm run emulators      # Start Auth + Firestore emulators (data persists across restarts)
npm run emulators:seed # Seed a synthetic test account (once) — test@example.com / test1234
npm run dev:emulator   # Run the app against the local emulators

# Browser tests (needs the emulators above running; app served on :3100, so your dev server can stay up)
npm run test:e2e       # Playwright: desktop 1440px, mobile 390px, degraded-state scenarios
npm run test:e2e:ui    # Same, interactive runner

# Speed and size (doc/guide/velocita.md): options always after `--`
npm run perf:budget    # JS per page of a production build against perf/budget.json, and one copy of recharts (after `npm run build`)
npm run perf:build     # Production build against the emulators, then `npm run perf:serve` on :3200
npm run perf:bench -- --runs=3  # Time to the first figure, cold and by navigation, per page
npm run perf:census -- --runs=5 # Components re-rendered per key / tab switch (after `npm run perf:build -- --profile`)

Server time, which the emulators cannot show, is read in production: GET /api/dashboard/overview answers with a Server-Timing header (auth, db, compute, total, and source = materialized or recompute), visible in DevTools → Network → the request → Timing. So do the Dividendi statistics (/api/dividends/stats), Rendimenti's yields (/api/performance/yields), the assistant's conversation list and the Esposizione's profiles (/api/portfolio/instrument-profiles, whose source says cache or yahoo).

See SETUP.md → Step 6 for the full local-emulator guide (prerequisites, persistence, reset) and SETUP.md → Step 7 for the Playwright suite.

Vitest covers the pure utilities and services, where the logic lives. Playwright covers what only a real browser can see — the desktop: layout switch at 1440px, animated disclosures, deep links that must cold-load into the right state, whether a loading state ever flashes the wrong content, and whether a page overflows its viewport at 390px (measured on the elements, since the app shell's scroll container hides that from document.scrollWidth). Covered pages: Previdenza (including a project for the states a healthy fixture can never reach — a return that can't be trusted, a window where nothing moved, a fund with no history yet — seeded on their own account: npm run e2e:seed -- suspicious|idle|fresh), Analisi (its own fixture account too, with every expense dated January so exact assertions hold whatever month the suite runs in) and FIRE / Coast FIRE (a deterministic Coast fixture with custom expenses, two state pensions and a pension fund unlocking inside the projection's horizon). Note: the emulators need Java 21+ — see SETUP.md → Step 6.

Conventions

  • UI language: Italian
  • Code language: English (comments explain WHY, not WHAT — see COMMENTS.md)
  • Responsive breakpoint: desktop: (1440px) instead of Tailwind's default lg:
  • Radix UI imports: All components/ui/ primitives import from the radix-ui umbrella package with named imports ({ X as XPrimitive }) — not from individual @radix-ui/react-* packages
  • Radix Select: No empty string values — use sentinel values like __all__
  • Modals: every dialog goes through components/ui/responsive-modal.tsx (Drawer ≤768px, Dialog above) with eyebrow / title / reading and one of four width steps; its words come from lib/utils/dialogNarrative.ts, never from JSX
  • Settings changes: Always update type definition + getter + setter together. A field the user can CLEAR needs the 'x' in settings guard in both write chains, or the write succeeds and the old value comes back on the next hard refresh

Deployment

Vercel (recommended)

  1. Import the repository on vercel.com
  2. Add all environment variables from .env.local
  3. Deploy — cron jobs for snapshots and dividends, and the region the functions run in (fra1, next to Firestore), are configured in vercel.json

Two cron jobs run daily at 18:00 UTC:

  • /api/cron/monthly-snapshot — Automatic monthly portfolio snapshots
  • /api/cron/daily-dividend-processing — Dividend data processing

For detailed instructions, see VERCEL_SETUP.md.

Docker (self-hosted)

Run the app on any VPS or server with Docker. Firebase still handles authentication and the database.

cp .env.local.example .env.local  # fill in your Firebase credentials
docker compose up -d --build

For the full guide including cron job setup and nginx/HTTPS configuration, see DOCKER.md.

Project Structure

net-worth-tracker/
├── app/                    # Next.js App Router
│   ├── api/                # API routes (17 endpoints)
│   ├── dashboard/          # Protected pages (8 sections)
│   ├── login/              # Auth pages
│   └── register/
├── components/             # React components (~116)
│   ├── ui/                 # shadcn/ui base components
│   ├── layout/             # Sidebar, header, navigation
│   ├── assets/             # Portfolio management
│   ├── performance/        # Metrics and charts
│   ├── cashflow/           # Income/expense tracking
│   ├── dividends/          # Dividend calendar and tables
│   ├── fire-simulations/   # FIRE calculator
│   ├── goals/              # Goal-based investing
│   ├── monte-carlo/        # Monte Carlo UI
│   ├── history/            # Historical analysis
│   ├── hall-of-fame/       # Rankings
│   └── pdf/                # PDF export (sections + primitives)
├── lib/
│   ├── services/           # Business logic (22 services)
│   ├── utils/              # Helpers (formatters, dates, auth)
│   ├── hooks/              # Custom React hooks
│   ├── constants/          # App config, colors, defaults
│   ├── firebase/           # Firebase client + admin setup
│   └── query/              # React Query key factory
├── types/                  # TypeScript definitions (9 files)
├── contexts/               # React contexts (AuthContext)
└── public/                 # Static assets

Contributing

Contributions are welcome! When contributing:

  1. Fork the repository and create a feature branch
  2. Follow the existing code conventions (Italian UI, English code)
  3. Read COMMENTS.md for the project's commenting philosophy
  4. Ensure npm run build passes before submitting a PR

If you work on this repo with an AI coding agent, point it at WORKFLOW.md first: it holds the standing session rules (one branch and one commit per session, never commit without explicit approval) and the guided-verification protocol used here — including how to drive the Firebase emulator and the authenticated Playwright fixtures instead of testing by hand.

Reporting Issues

Known Issues

  • Currency conversion depends on the Frankfurter API (falls back to 24h-cached rates); non-EUR assets created before the FX update will show native price as EUR until the next price refresh
  • Demo account requires manual setup: create a Firebase user, populate Firestore with realistic fake data, and set the three NEXT_PUBLIC_DEMO_* env vars

License

This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).

This means you are free to use, modify, and distribute this software, but any modified version that is accessible over a network must also make its source code available under the same license.

See LICENSE.md for the full license text.

Screenshots

Screenshots recorded on the app with anonymized or synthetic data.

Dashboard & Portfolio

Portfolio overview Overview: the month's verdict over a grid of tiles — net worth, liquidity, cashflow, composition, costs, goals

Asset allocation Allocazione: the verdict over Bilanciamento, Piano, Per classe and Esposizione

Cashflow

Cashflow Sankey Analisi: the flow of the year as a 5-layer Sankey inside its tile — a type drills by a click, a category opens its Scheda

Cashflow drill-down Analisi: the Scheda of a subcategory — period total, per-year table with signed deltas, 24-month trend, transactions

Performance & History

Performance metrics Rendimenti: the verdict — how much the portfolio returns, and against what — over the TWR, the risk figures and the six model portfolios

Monthly heatmap Rendimenti: the Consistenza tile — positive months over the measured ones, and every month's return as a heatmap

Net worth history Net worth evolution over time

FIRE & Simulations

FIRE calculator FIRE projections with Bear/Base/Bull scenarios

Monte Carlo FIRE › Monte Carlo — the verdict over Probabilità, Distribuzione and Scenari a confronto

Dividends & Hall of Fame

Dividend calendar Dividendi: the payments tile in calendar view — received and expected kept apart, day by day

Hall of Fame Hall of Fame: the record month named first, then the rankings — net worth, income, what a month kept, and the years

Star History

Star History Chart

About

Track your net worth and investment portfolio with automatic price updates, asset allocation, expense tracking, and FIRE calculator. Modern alternative to spreadsheets. Next.js + Firebase + TypeScript.

Topics

Resources

Stars

77 stars

Watchers

3 watching

Forks

Releases

Contributors

Languages