Skip to content
oiibarPublic

About

A full-stack personal finance and budget management platform featuring income and expense tracking, customizable categories, monthly budgets, financial analytics, and CSV exports. Built with React, TypeScript, NestJS, PostgreSQL, Redux Toolkit, and Docker.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

Wisely

A personal budget manager: record income and expenses in your own categories, set a monthly limit per category, and see at a glance where the month stands. React + NestJS + PostgreSQL, packaged with Docker.

Status: v1 rework on the feat/v1-rework branch (not yet merged or released). API and web client are implemented and tested locally. There is no live demo: Wisely runs locally with Docker Compose, and no hosting has been chosen.

The problem

Tracking money by hand in a spreadsheet works until categories drift, entries get duplicated and "how much is left for groceries this month" needs a formula someone has to maintain. Wisely keeps that bookkeeping simple and private: no bank connections, just your own transactions, categories and budgets, with correct totals and an export whenever you want your data back.

Features

  • Transactions: income and expense with title, amount, date, optional category and note. Filter by date range, type and category (or uncategorised), search title and note, sort by date or amount, paginate. Retried submits never create duplicates (idempotent on a client id).
  • Categories: your own, case-insensitively unique, with transaction counts. Deleting one keeps its transactions as uncategorised.
  • Monthly budgets: a limit per category and month with spent and remaining amounts (negative when over budget).
  • Dashboard: month income, expenses and balance, budget progress, a monthly income-vs-expense chart and spending by category.
  • CSV export of the filtered transactions, safe to open in a spreadsheet.
  • Accounts: sign up, log in, e-mail verification, password reset by e-mail, change username, display currency and password, log out everywhere, delete the account and all its data.

Pages: / landing, /login, /signup, /forgot-password, /reset-password, /verify-email, /dashboard, /transactions, /categories, /budgets, /account, and a 404 page. Details in docs/product-requirements.md.

Screenshots

Captured from the local Docker stack with the deterministic seed data (client/scripts/capture-screenshots.mjs); nothing is mocked.

Dashboard Transactions
Dashboard: month income, spending and balance, spending by category, budget progress and a six-month chart Transactions: filters, sortable table, export and add buttons
Budgets New transaction
Budgets: monthly limits per category with progress bars Transaction dialog with type, title, amount, date, category and note

Mobile: docs/screenshots/mobile-dashboard.png. The original 2025 UI is kept for comparison in previews/wisely.png.

Live demo

None. Wisely is not deployed anywhere; see Deployment.

Technology stack

Layer Choice Why
Web React 19, React Router 7, Redux Toolkit + RTK Query, Vite 7, Tailwind CSS 3, Recharts Kept from the original app; RTK Query gives caching, tag invalidation and one place for the refresh-on-401 logic
API NestJS 11, TypeORM 0.3, class-validator, Joi Kept from the original; modules, guards and DTO validation fit a modular monolith
Database PostgreSQL 17 Relational data with real constraints (composite ownership foreign keys, checks) and SQL aggregates for reports
Auth Argon2id, HS256 JWT access cookie + rotating opaque refresh token HttpOnly cookies keep tokens away from scripts; rotation makes sessions revocable
Mail Nodemailer over SMTP; Mailpit locally No paid service needed in development
Ops Docker, Docker Compose, nginx, Caddy (prepared), GitHub Actions, GHCR One host, reproducible images, automatic TLS when deployed
Observability pino, prom-client, Terminus, optional Sentry Structured logs with request ids, RED metrics, health probes
Tests Jest + Supertest against real PostgreSQL, Vitest + Testing Library, Playwright + axe Real database for API tests; browser tests against the full stack

Decisions are recorded as ADRs in docs/adr.

Architecture

flowchart LR
  B((Browser)) -- "same origin" --> E["nginx (local) / Caddy (prod)"]
  E -- "/api/*" --> A["NestJS API<br/>auth · users · categories ·<br/>transactions · budgets · reports"]
  E -- "everything else" --> W["Static SPA<br/>(React)"]
  A --> D[("PostgreSQL<br/>users · categories · transactions ·<br/>budgets · auth_tokens")]
  A --> M["SMTP<br/>(Mailpit locally)"]
Loading

The browser only talks to one origin; /api is proxied to the API, so session cookies are first-party. Every query is scoped by the logged-in user, money is stored as integer cents, and totals are computed in SQL. More in docs/architecture.md and docs/diagrams.

Local setup

Requirements: Docker with the Compose plugin. For running the apps on the host as well: Node.js 20+ (CI uses 24).

git clone https://github.com/oiibar/Wisely.git
cd Wisely
cp .env.example .env              # then set JWT_SECRET (see below)
docker compose up -d --build --wait
docker compose run --rm migrate
docker compose run --rm seed      # optional demo data
Service URL
Web http://localhost:3200
API (direct) http://localhost:4200/api/v1, Swagger at http://localhost:4200/api/docs
Mailpit (caught e-mails) http://localhost:8027 (SMTP on 127.0.0.1:1027)
PostgreSQL 127.0.0.1:5435, user/password/db wisely

After seeding, log in as demo@wisely.dev / Demo-password-1 (six months of data and budgets) or alex@wisely.dev / Demo-password-1 (nearly empty). These are fictional development accounts.

Hot reload on the host (database and Mailpit still in Docker):

docker compose up -d db mailpit
cd server && cp .env.example .env && npm ci     # set JWT_SECRET in server/.env
npm run migration:run && npm run seed && npm run start:dev   # API on :4200
cd ../client && npm ci && npm run dev           # web on :3200, proxies /api to :4200

Environment variables

  • .env (repository root, from .env.example) configures Docker Compose. Only JWT_SECRET is required; generate it with node -e "console.log(require('crypto').randomBytes(48).toString('hex'))".
  • server/.env (from server/.env.example) configures the API when it runs on the host.

Every variable is validated at start-up by server/src/config/env.validation.ts; the API refuses to boot on a missing or malformed value. Notable ones: DATABASE_URL, JWT_SECRET (≥ 32 characters), APP_URL and CORS_ORIGINS, ACCESS_TOKEN_TTL / REFRESH_TOKEN_TTL (default 15m / 30d), MAIL_TRANSPORT (log or smtp) with SMTP_*, THROTTLE_LIMIT / AUTH_THROTTLE_LIMIT, SENTRY_DSN (optional). Never commit .env files.

Database migrations and seed

The schema is defined by migrations in server/src/database/migrations; TypeORM synchronize is off.

# Docker
docker compose run --rm migrate          # apply
docker compose run --rm migrate migrate-revert   # undo the latest
docker compose run --rm seed             # demo data (development only)

# Host (server/)
npm run migration:run
npm run migration:revert
npm run migration:generate -- src/database/migrations/<Name>
npm run seed

The seed is idempotent and deterministic. Schema, indexes and rules are in docs/database.md.

Tests and quality checks

# server/
npm run format:check && npm run lint && npm run typecheck
npm test               # unit tests
npm run test:e2e       # API tests against PostgreSQL (database wisely_test on :5435)
npm run build

# client/
npm run format:check && npm run lint && npm run typecheck
npm test               # unit + component tests (Vitest)
npm run test:e2e       # Playwright against the running stack on :3200
npm run build

Create the test database once with docker compose exec db psql -U wisely -c "CREATE DATABASE wisely_test". CI runs all of the above plus image scanning (Trivy) and secret scanning (Gitleaks). What each suite covers, and current gaps, are in docs/testing.md. No pass counts or coverage figures are claimed here.

Deployment

Wisely currently runs locally only. A single-host production setup is prepared but has never been used on a real server: compose.prod.yml adds Caddy with automatic HTTPS, removes all other published ports, forces secure cookies and real SMTP, and disables Swagger. Tagging vX.Y.Z builds and publishes images to GHCR; the deploy job (backup, migrate, roll out, smoke test over SSH) stays skipped until a host is configured. See docs/deployment.md, docs/operations-runbook.md and docs/disaster-recovery.md.

Security

  • Argon2id password hashing; the same error and timing for unknown e-mails and wrong passwords.
  • HttpOnly SameSite=Lax cookie sessions with rotating refresh tokens and reuse detection; password change or reset revokes every session.
  • CSRF origin check on cookie-bearing writes, rate limits (stricter on auth endpoints), strict input validation, Helmet and nginx security headers.
  • Ownership enforced on every query (404 for other users' data) and by composite foreign keys in the database.
  • CSV export neutralises spreadsheet formula injection.
  • Secrets only in environment variables; Gitleaks in CI; logs redact credentials and tokens.

Details in docs/security.md. Report vulnerabilities as described in SECURITY.md.

Known limitations and future improvements

  • Leaked credential in Git history: commit c9c6c46 (2025) contains a hosted database password in server/src/app.module.ts. It is public and must be rotated by the owner at the provider; see SECURITY.md.
  • The v1 web client is still being rebuilt; the screenshot above is the old UI.
  • No deployment, so no measured latency, uptime or restore times. Performance has not yet been measured for Wisely (docs/performance.md).
  • Single display currency per user; amounts are never converted and every currency is treated as having two decimals.
  • No recurring transactions, statement import, shared budgets, MFA or e-mail change flow.
  • Rate limits are in memory per API process; multiple replicas would need Redis.
  • Offset pagination; keyset pagination and trigram search indexes are the next steps for very large histories.

Contributing

See CONTRIBUTING.md and CHANGELOG.md.

License

No license has been granted. All rights reserved. The source is visible for review, but you may not use, copy, modify or distribute it without the author's permission.

About

A full-stack personal finance and budget management platform featuring income and expense tracking, customizable categories, monthly budgets, financial analytics, and CSV exports. Built with React, TypeScript, NestJS, PostgreSQL, Redux Toolkit, and Docker.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages