尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗A private, mobile-first web app for tracking investment accounts, valuations, contributions, holdings and performance. GBP by default, UK date formats.
Stack: Next.js 15 (App Router, TypeScript) · Tailwind CSS + shadcn/ui-style components · Supabase (Postgres, Auth, RLS) · Recharts · decimal.js · Vitest · Vercel.
You can try everything without any setup: run it locally and choose Explore with demo data. Demo data lives only in your browser's storage and never touches the database.
/api/cron/daily): stores each investment's closing price in price_history (UK funds backfill a year on the first run) and saves the calculated value of every holdings-valued account as a snapshot. Each holding shows a value-over-time chart and 1M/3M/6M/1Y price returns.Full explanation is in the app under Settings → How the numbers are calculated. In short:
| Concept | Rule |
|---|---|
| Account value | Exactly one value per account: the latest snapshot (manual accounts) or holdings × price + cash (calculated accounts). Holdings in a manually valued account are compared with the snapshot but never added on top. |
| External flows | Only deposits and withdrawals. Transfers between your own accounts count for each account but cancel out in portfolio totals. Buys/sells/dividends/interest/fees are never contributions. |
| Starting point | Contributions recorded before the first valuation → start from £0. Otherwise the first valuation (less any contributions that same day) is the opening balance. |
| Gain/loss | current value + withdrawals − contributions − opening balance |
| TWR | Valuations split history into sub-periods; each uses Modified Dietz with day-weighted flows; sub-periods are chain-linked. Marked Estimate when inputs are interpolated or large flows fall in long gaps. Annualised once a year of data exists. |
| XIRR | Money-weighted annual return on your actual cash flows (Newton–Raphson with bisection fallback). |
| History | Between valuations, values are interpolated (adjusted for flows in between) and shown as hollow chart points. |
| Precision | All arithmetic via decimal.js; database uses numeric, never float. |
The calculation engine lives in src/lib/finance/ and is pure TypeScript with no UI dependencies.
npm test # unit tests: money, flows, TWR, XIRR, holdings, portfolio model, CSV, price parsing
npm run typecheck
npm run lint
Tests cover decimal safety, flow classification (transfers and trades are not contributions), the gain formula, TWR (including a deposit-neutral case), XIRR against a published spreadsheet example, average-cost holdings with fees, pence/USD conversion, no double counting of holdings vs snapshots, interpolation, and same-day valuation/contribution ordering.
All tables live in their own finance Postgres schema, so the app can share a Supabase project with other apps (it currently shares the My House Dashboard project) without touching their tables. Sign-in uses that project's existing accounts.
supabase/migrations/ (in order) into SQL Editor → New query and run it; ornpx supabase link --project-ref <ref> then npx supabase db push.finance (keep the schemas already listed). Without this the app can't reach its tables.http://localhost:3000/** and https://your-app.vercel.app/** to Redirect URLs. Leave the Site URL as it is if another app already uses it.The schema creates accounts, account_snapshots, assets, holdings, transactions, market_prices, fx_rates, credit_cards and credit_card_bills, with foreign keys, check constraints and indexes. Row Level Security is enabled on every table: each user can only read and write their own rows, and child rows can only reference the same user's parents (composite foreign keys). market_prices is readable by signed-in users and writable only with the service role. A create_transfer function writes both legs of a transfer atomically. The schema doesn't add triggers to auth.users or anything to public.
There's no SQL seed for real data on purpose: demo data is generated in the browser (src/lib/data/demo.ts) so it can never mix with your records.
Copy .env.example to .env.local and fill in:
| Variable | Where | Required | Get it from |
|---|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | browser + server | For real data | Supabase → Project Settings → API → Project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY | browser + server | For real data | Same page → anon public key (safe to expose; RLS protects data) |
SUPABASE_SERVICE_ROLE_KEY | server only | For the daily job | Same page → service_role (secret) key. Lets the server write the shared price cache, price history and daily valuations. Never expose it. |
CRON_SECRET | server only | For the daily job | Any long random string. Vercel sends it to /api/cron/daily; other callers are refused. |
ALPHA_VANTAGE_API_KEY | server only | For live UK prices | Free key at alphavantage.co |
FINNHUB_API_KEY | server only | Optional | Free key at finnhub.io (US tickers) |
EODHD_API_KEY | server only | For UK fund prices | Free key at eodhd.com (unit trusts/OEICs by ISIN) |
Without the Supabase variables the app runs in demo mode only. Without SUPABASE_SERVICE_ROLE_KEY and CRON_SECRET the daily history job doesn't run. Without market data keys, prices fall back to manual prices (or cost) and are labelled.
npm install
cp .env.example .env.local # optional: add Supabase + API keys
npm run dev # http://localhost:3000
Choose Explore with demo data on the sign-in page to use sample data. Settings → Reset demo data restores it.
vercel.json pins the framework to Next.js, so an existing project set up for the old Vite app builds correctly.vercel.json (21:00 UTC, Monday to Friday) and needs SUPABASE_SERVICE_ROLE_KEY and CRON_SECRET.Researched October 2026 (free tiers):
| Provider | Free allowance | UK (LSE) | US | Freshness |
|---|---|---|---|---|
| Alpha Vantage (used) | 25 requests/day, 5/min | Yes (VUSA.LON) | Yes | Delayed / end of day |
| Finnhub (optional, US) | 60 requests/min | No (paid) | Yes | Delayed |
| EODHD (optional, UK funds) | 20 requests/day | Funds by ISIN (EUFUND) | — | End of day |
| Twelve Data | 800/day, 8/min | No (US only on free plan) | Yes | — |
market_prices table, at most 5 symbols are fetched per refresh, and only signed-in users can call the price API. With many holdings, refresh in batches or set manual prices for funds.GB00BG0QP604) as the ticker and add an EODHD_API_KEY to fetch its daily price; the form assumes pence (GBX), so compare the first price with your platform and switch Quoted in to GBP if it's 100× off. Without a key, use a manual price.src/
app/ routes (dashboard, accounts, holdings, activity, insights, settings, login, API)
components/ UI primitives (ui/), charts, forms, layout, shared lists
lib/finance/ calculation engine + tests
lib/market/ price providers, cache, FX, market hours (server)
lib/data/ repositories (Supabase / browser demo) and demo data
supabase/migrations/ database schema
The earlier Vite + React app was rebuilt on Next.js so price API keys stay on the server. Its features (pots with values and contributions, share transactions, credit card bills) are covered by accounts, snapshots, transactions, holdings and the credit cards page. One behavioural change: data is now private per user (the old schema shared all rows between any signed-in users). The old schema files are in git history; there was no existing database to migrate.