A self-hosted personal debt payoff tracker focused on freeing up monthly cash flow as fast as possible. React frontend + a tiny Node/Express backend, JSON-file storage, HTTP Basic Auth. No database, no third-party services — your financial data stays on your own server.
The debts in the app on first run are placeholder examples. Edit them in the UI (or in
src/App.jsx) to match your own. Your real data is saved to a JSON file that is gitignored.
Tracking
- Per-debt cards with balance, APR, minimum payment, and progress to payoff
- Log a single payment to one account, or log all balances at once
- Payment history showing which account each entry was applied to
- Dark and light themes (remembers your choice; respects system preference)
Dynamic minimum payments
- Revolving cards compute their minimum as
max(floor, rate% × balance), so it floats down automatically as you pay it off - Installment loans (auto loans, payment plans, BNPL) use a fixed monthly payment
- Each debt's type, rate, and floor are editable per debt
Interest modeling
- Revolving-card balances accrue an estimated interest overlay between statements (marked
est.), which reconciles whenever you log an actual balance - Installment loans follow their fixed schedule and don't accrue an estimate
- Lifetime interest paid and "interest saved" vs. an interest-only baseline
Payoff strategy & planning
- Three payoff orders compared side by side: Cash flow (most monthly payment freed per dollar), Snowball (smallest balance first), and Avalanche (least total interest)
- Month-by-month simulation projects real payoff dates and total interest at your chosen monthly budget
- A "this month" action card tells you exactly what to pay and to which account
- "Monthly payments freed over time" chart visualizes the cash flow you're buying back
- Windfall allocator: enter a lump sum and see it cascade down your payoff order with the interest/time saved
- Low-rate installment loans can be flagged
excludeFromPlanto keep them out of the consumer payoff plan while still counting in your totals
Financial health
- Credit utilization per card and overall (when credit limits are entered)
- Interest-vs-principal breakdown of everything you've paid
- Frontend: React 18 + Vite, Recharts, lucide-react (no CSS framework — inline-styled)
- Backend: Node.js + Express,
express-basic-auth - Storage: a single JSON file on disk
npm install
cp .env.example .env
# edit .env and set TRACKER_PASSWORD
# terminal 1: backend API
npm run dev:server
# terminal 2: frontend with hot reload
npm run dev:clientOpen the Vite dev URL it prints; /api/* is proxied to the backend.
npm install
npm run build # builds the frontend into dist/ (runs an undefined-reference check first)
npm start # serves dist/ AND the API on PORT (default 3000)The app is designed to run behind a reverse proxy that terminates TLS and enforces auth. Example nginx and Apache vhosts are in deploy/, plus a hardened systemd unit. Typical setup:
- Copy the project to your server and
npm install && npm run build. - Run it under
systemdas a dedicated user (seedeploy/debt-tracker.service). - Front it with nginx/Apache, add Basic Auth, and point a domain or sub-path at
127.0.0.1:3000(seedeploy/nginx.conf). - Get a TLS cert (e.g. Let's Encrypt).
To serve under a sub-path (e.g. /tracker), set base in vite.config.js and have the proxy strip the prefix.
Environment variables (see .env.example):
| Var | Default | Purpose |
|---|---|---|
TRACKER_PASSWORD |
— | Basic-auth password. If unset, the app's own auth layer is disabled (use only when your proxy already enforces auth). |
TRACKER_USER |
tracker |
Basic-auth username |
PORT |
3000 |
Port to listen on |
DATA_DIR |
./data |
Where tracker.json is stored |
DATA_DIR/tracker.json — a single JSON file with your debts and logged balances. It is gitignored. Back it up periodically (POST /api/backup writes a timestamped copy, or just rsync the file).
- This is sensitive financial data. Always serve it over HTTPS, behind authentication, and ideally not indexable by search engines (
X-Robots-Tag: noindex). - The backend keeps request bodies out of logs to avoid leaking values.
- Nothing leaves your server — there are no analytics or external calls (the UI loads Google Fonts; self-host them if you want zero external requests).
- The committed
src/App.jsxships placeholder example debts; your real numbers live only in the gitignored data file.
Personal project — use it however is useful to you.