A left-to-right budget tree webapp:
- Vue 3 (Composition API) frontend
- Node.js + TypeScript (Express) backend
- MySQL storage
| Level | Content |
|---|---|
| 1 | Gross Income — enter yearly amount; tree shows monthly (÷ 12) |
| 2 | Income Tax, Retirement, Insurance, Net Income |
| 3 | Tax withholdings (Fed W/H, Soc Sec, Medicare, State W/H, …); retirement plans (% of gross, e.g. 401k / Roth); insurance types; budget categories under Net |
| 4 | Expenses under each category |
Input units
- Tax & insurance: enter biweekly paycheck amounts → tree shows monthly (
biweekly × 26 ÷ 12) - Retirement: enter % of gross per plan → tree shows monthly dollars
- Expenses / categories: monthly amounts
Every node shows its percentage of the parent tier.
- Node.js 20+
- MySQL 5.7+ / 8.x
Create a database (example already used in this project: budget_app):
CREATE DATABASE IF NOT EXISTS budget_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Configure credentials in backend/.env:
PORT=3040
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=budget_app
# Required — signs session tokens after a successful PIN entry
SESSION_SECRET=change-me-to-a-long-random-string# From repo root
npm install
npm install --prefix backend
npm install --prefix frontend
# Create / migrate tables
npm run db:init
npm run db:migrate
# Set the 6-digit app PIN (stored as a salted scrypt hash in MySQL only)
npm run set-pin -- 123456
# or, hide from shell history:
# npm run set-pin -- --stdin
# API + Vite dev servers
npm run dev- Frontend: http://localhost:5180
- API: http://localhost:3040
Vite proxies /api to the backend.
The UI and all /api/budget/* routes require a 6-digit PIN.
The PIN is never stored in plaintext. Only a salted scrypt hash is kept in app_security.pin_hash. Set or change it with the backend CLI (not .env):
npm run set-pin -- 123456
# npm run set-pin -- --stdin- Open the app → enter PIN on the lock screen.
- A session token is stored in
sessionStorageand sent asAuthorization: Bearer …. - 5 wrong attempts sets
app_security.is_locked = 1. Further unlocks are rejected until you clear it in MySQL:
UPDATE app_security
SET is_locked = 0, failed_attempts = 0
WHERE id = 1;Changing the PIN with set-pin does not clear lockout counters — use the SQL above if locked out.
| Method | Path | Description |
|---|---|---|
GET |
/api/auth/status |
Lock state + whether current token is valid |
POST |
/api/auth/verify |
{ pin } → { token } (public) |
GET |
/api/budget/tree |
Full computed tree + totals (auth required) |
PUT |
/api/budget/settings |
{ gross_income_yearly } |
CRUD |
/api/budget/tax-items |
Biweekly tax withholdings |
CRUD |
/api/budget/retirement |
Retirement plans (name, % of gross) |
CRUD |
/api/budget/insurance |
Biweekly insurance payroll items |
CRUD |
/api/budget/categories |
Budget categories (is_essential) |
CRUD |
/api/budget/expenses |
Expenses under a category |
Budget routes return 401 without a valid token and 423 when lockout is active.
- Gross monthly = yearly ÷ 12
- Tax monthly = sum of biweekly tax items × 26 ÷ 12
- Retirement monthly = gross monthly × (sum of plan %)
- Insurance monthly = sum of biweekly insurance items × 26 ÷ 12
- Net = gross − tax − retirement − insurance (monthly)
- Category totals = sum of expense amounts; unallocated = net − category totals
- Click Gross Income and set yearly salary.
- Click Income Tax and enter biweekly Fed W/H, Soc Sec, Medicare, State W/H (defaults are pre-seeded).
- Click Retirement and set plan percentages (defaults: 401k 6%, Roth 4%).
- Add Insurance types with biweekly amounts.
- Under Net Income, add categories (toggle essential ★).
- Under each category, add monthly expenses.
Percent badges on each card are relative to the parent node.