A simple personal finance tracker built with React + TypeScript (frontend) and Node.js + Express + MySQL (backend).
Great for tracking income, expenses, budgets, savings/investments, and recurring bills — with charts and export/import.
- 🔐 Register & Login (passwords hashed with bcrypt)
- 📊 Dashboard: income vs expense, balance summary, recent transactions
- 💵 Income: CRUD + categories + payment methods
- 💸 Expenses: CRUD + categories + payment methods
- 🎯 Budgets per category and month
- 🐷 Savings / Investments tracking
- 📅 Bills (recurring) + payment history
- 📈 Insights & Heatmap (spending patterns)
- 🧾 Reports + Export to PDF
- ♻️ Export/Import JSON for backup/restore
- ✅ Base currency: IDR (amounts are stored in IDR)
- 🌍 Choose a display currency (IDR, USD, EUR, GBP, SGD, JPY, and more)
- 🔄 Automatic conversion using the latest exchange rates + a Refresh rates action
- 🕒 Rates are cached (lightweight and fast), with manual refresh whenever needed
- 🌐 Multi-language UI (base language:
id) — EN, ES, FR, DE, PT, RU, AR, HI, 中文, 日本語, 한국어 - ⚙️ Auto sync & auto translate for new/changed keys (hash-based, so it avoids unnecessary work)
- 🐳 LibreTranslate via Docker support for auto-translation without paid APIs (optional)
- Frontend: React 18, TypeScript, Vite, Tailwind, shadcn/ui, Zustand, React Router
- Charts: Recharts
- PDF Export: jsPDF + jspdf-autotable
- Currency: Zustand store + persisted settings + exchange rates (public currency rates API)
- Backend: Node.js, Express, mysql2, bcryptjs, uuid
- Container: Docker + Docker Compose (Nginx serves the frontend + reverse-proxy
/api)
my-local-wallet/
├─ src/ # React app
├─ backend/ # Express API + MySQL schema/init
├─ docker-compose.yml # app + backend + mysql (+ libretranslate + i18n tools)
├─ nginx.conf # proxy /api -> backend
├─ Dockerfile # build React -> serve via Nginx
├─ backend/Dockerfile # backend image
├─ .env.example # env template (copy to .env)
└─ README_DOCKER.md # Docker quick notes
- Copy the env file:
cp .env.example .env- Edit
.env(at minimum: MySQL credentials). Example:
WEB_PORT=3000
MYSQL_ROOT_PASSWORD=change_me_root
MYSQL_DATABASE=finance_db
MYSQL_USER=finance_user
MYSQL_PASSWORD=change_me_password- Run:
docker compose up -d --build- Open:
- Frontend: http://localhost:3000
- Backend health: http://localhost:3000/api/health
Note: In Docker, the frontend uses
VITE_API_URL=/apiand Nginx proxies/api/*to the backend container.
docker compose down -v
docker compose up -d --build- Node.js 18+ (Docker uses Node 20)
- MySQL 8.0+
cd backend
npm install
# set env then run:
MYSQL_HOST=127.0.0.1 MYSQL_PORT=3306 MYSQL_USER=root MYSQL_PASSWORD=your_password MYSQL_DATABASE=finance_db npm run devBackend runs on: http://localhost:3001
Open a new terminal:
npm install
# point frontend to backend:
export VITE_API_URL="http://localhost:3001/api"
npm run devFrontend runs on: http://localhost:5173
VITE_API_URL
Base URL for the backend API.- Local dev:
http://localhost:3001/api - Docker:
/api(default in Dockerfile)
- Local dev:
WEB_PORT(default3000) — exposed port for the web UIMYSQL_ROOT_PASSWORDMYSQL_DATABASEMYSQL_USERMYSQL_PASSWORDMYSQL_HOST(Docker usesmysql)MYSQL_PORT(default3306)
- All amounts are stored in IDR, then displayed in the user-selected display currency.
- Rates are fetched from a public exchange-rates API and cached to reduce repeated calls.
- If rates cannot be fetched (e.g., offline), the app still works and falls back to IDR / last-known rates.
GET /api/healthPOST /api/auth/registerPOST /api/auth/login- CRUD:
/api/incomes/*/api/expenses/*/api/budgets/*/api/savings/*/api/master_data/*/api/bills/*/api/bill_payments/*
- Passwords are hashed (bcrypt) before being stored.
- For production: change DB passwords, and run behind HTTPS (reverse proxy like Nginx/Caddy/Traefik).
- Default CORS is enabled (backend uses
cors()).
Base translations live in:
src/locales/id.json✅ (source of truth)
Other languages:
src/locales/en.json,es.json,fr.json,de.json,pt.json,ru.json,ar.json,hi.json,zh.json,ja.json,ko.json
- Add/update keys in
src/locales/id.json - Run the app:
npm run devThe system will:
- sync locale files (create missing language files)
- auto-translate new/changed keys (when a translator service is available)
npm run i18n:sync
npm run i18n:translate
npm run i18n:autoIf you’re using Docker Compose, you can enable LibreTranslate for automatic translations without a paid API.
In docker-compose.yml, these services are already included:
libretranslatei18n_bootstrap(one-shot generation)i18n_watcher(periodic sync/translate)
Default provider: LibreTranslate (free). (Optional) you can switch to another provider via the
TRANSLATE_PROVIDERenv variable.
See LICENSE.