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-reworkbranch (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.
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.
- 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.
Captured from the local Docker stack with the deterministic seed data
(client/scripts/capture-screenshots.mjs); nothing is mocked.
| Dashboard | Transactions |
|---|---|
![]() |
![]() |
| Budgets | New transaction |
![]() |
![]() |
Mobile: docs/screenshots/mobile-dashboard.png. The original 2025 UI is kept for comparison in previews/wisely.png.
None. Wisely is not deployed anywhere; see Deployment.
| 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 |
| 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.
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)"]
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.
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.env(repository root, from.env.example) configures Docker Compose. OnlyJWT_SECRETis required; generate it withnode -e "console.log(require('crypto').randomBytes(48).toString('hex'))".server/.env(fromserver/.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.
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 seedThe seed is idempotent and deterministic. Schema, indexes and rules are in docs/database.md.
# 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 buildCreate 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.
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.
- Argon2id password hashing; the same error and timing for unknown e-mails and wrong passwords.
- HttpOnly
SameSite=Laxcookie 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 (
404for 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.
- Leaked credential in Git history: commit
c9c6c46(2025) contains a hosted database password inserver/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.
See CONTRIBUTING.md and CHANGELOG.md.
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.



