A full-stack personal finance management application built with Django REST Framework (backend) and Next.js (frontend).
This project is not deployed and has no live demo. It is a portfolio/learning project that runs fully locally via Docker Compose (see Getting Started). The deployment notes below describe a plan and have not been tested in production.
- Django 5.0.6 - Web framework
- Django REST Framework 3.15.2 - API framework
- SimpleJWT 5.3.1 - JWT authentication (with token blacklist)
- MySQL 8.x - Database (SQLite in-memory for tests)
- django-filter 24.2 / drf-spectacular 0.27.2 - Filtering & API docs
- gunicorn 26.2.0 + whitenoise 6.12.0 - Production server & static files
- pytest + pytest-django + factory-boy + freezegun - Tests
- Next.js 14.2.3 - React framework (App Router)
- TypeScript 5.5.4 - Type safety
- Tailwind CSS 3.4.7 - Styling
- TanStack Query 5.51.15 - Server state management
- Zustand 4.5.4 - Client state management
- React Hook Form + Zod - Form validation
- Recharts - Data visualization
- Vitest 5 - Unit tests (
lib/utils)
- Frontend: Vercel (Next.js)
- Backend: Any container host with gunicorn + WhiteNoise (e.g. Render)
- Database: Managed MySQL 8
- π Authentication - JWT auth (register, login, logout with real token revocation, refresh, change password, delete account)
- π° Transactions - CRUD for income/expense transactions with year/month/type filters and a 6-month trend endpoint
- π·οΈ Categories - Custom categories with icons & colors; unpaginated list; duplicate names rejected; deleting a category with transactions returns 409
- π Dashboard - Monthly summary, 6-month trend chart (real data), category breakdown (per selected month)
- π± Responsive - Mobile-first design
finance-app/
βββ backend/ # Django project
β βββ config/ # Settings, settings_test, URLs
β βββ core/ # User model, auth views (register/login/refresh/logout/me/change-password)
β βββ transactions/ # Category & Transaction models, filters, trend endpoint
β βββ assets/ # Empty placeholder package β not in INSTALLED_APPS
β βββ tests/ # pytest suite (113 tests, SQLite in-memory)
β βββ requirements.txt # Runtime dependencies
β βββ requirements-dev.txt # + pytest, factory-boy, freezegun, pip-audit
β βββ Dockerfile # Dev image (runserver via entrypoint)
β βββ Dockerfile.prod # Production image (gunicorn, non-root)
β βββ manage.py
βββ frontend/ # Next.js project
β βββ src/
β β βββ app/ # App Router pages
β β βββ components/ # Reusable components
β β βββ lib/ # API client, utilities
β β βββ hooks/ # Custom React hooks
β β βββ types/ # TypeScript types
β βββ package.json
β βββ tailwind.config.ts
β βββ Dockerfile
βββ docker-compose.yml # Local development
βββ README.md
- Docker & Docker Compose
- Node.js 20+ (for local frontend development)
- Python 3.12+ (for local backend development)
- Clone the repository
- Copy environment files:
cp backend/.env.example backend/.env
- Start all services:
docker-compose up --build
- Access the application:
- Frontend: http://localhost:3000
- Backend API: http://localhost:8000/api
- API Docs: http://localhost:8000/api/docs/ (DEBUG=1 only)
cd backend
python -m venv venv
source venv/bin/activate # or venv\Scripts\activate on Windows
pip install -r requirements-dev.txt # includes runtime + test deps
cp .env.example .env
# Edit .env with your database settings
python manage.py migrate
python manage.py seed_default_categories
python manage.py runservercd frontend
npm ci
cp .env.example .env.local
# Edit .env.local with your API URL
npm run devBackend (no MySQL needed β tests use SQLite in-memory):
cd backend
pytest -q -p no:warnings # 113 tests
python manage.py makemigrations --check --dry-run --settings=config.settings_testFrontend:
cd frontend
npm run lint
npm run type-check
npm run test # vitest (lib/utils)
npm run build| Variable | Default | Notes |
|---|---|---|
DEBUG |
0 |
Must be 1 for dev; when 0 the app fails fast without a secure SECRET_KEY + ALLOWED_HOSTS |
SECRET_KEY |
β | Required when DEBUG=0; must not start with django-insecure |
ALLOWED_HOSTS |
β | Comma-separated; required when DEBUG=0 |
DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD |
localhost / 3306 / finance_db / root / root | MySQL connection |
CORS_ALLOWED_ORIGINS |
http://localhost:3000 |
Comma-separated allowlist |
SECURE_SSL_REDIRECT |
1 when DEBUG=0 |
|
SESSION_COOKIE_SECURE / CSRF_COOKIE_SECURE |
1 when DEBUG=0 |
|
SECURE_HSTS_SECONDS |
0 |
Raise to e.g. 31536000 once HTTPS is confirmed |
BEHIND_PROXY |
0 |
Set 1 behind a TLS-terminating proxy |
ENABLE_API_DOCS |
β | 1 exposes /api/docs even when DEBUG=0 |
THROTTLE_ANON_RATE / THROTTLE_USER_RATE / THROTTLE_AUTH_RATE |
60/min / 300/min / 10/min |
Auth scope covers register/login/refresh/change-password |
RUN_MIGRATIONS |
1 |
0 skips migrations in the entrypoint |
WEB_CONCURRENCY |
3 |
Gunicorn worker count |
NEXT_PUBLIC_API_URL=http://localhost:8000/apiPOST /api/auth/register/- Register new user (returns tokens for auto-login)POST /api/auth/login/- Login (returns access & refresh tokens + user)POST /api/auth/refresh/- Refresh access tokenGET /api/auth/me/- Get current userPATCH /api/auth/me/- Update profile (first_name, last_name, username, email β unique, case-insensitive email)POST /api/auth/change-password/- Change password (blacklists all refresh tokens, returns a new pair)DELETE /api/auth/me/- Delete account (body:{ "password": "..." })POST /api/auth/logout/- Logout (blacklists refresh token)
GET /api/categories/- List all categories (user + defaults, unpaginated)POST /api/categories/- Create category (duplicate name+type β 400)GET /api/categories/{id}/- Get categoryPATCH /api/categories/{id}/- Update category (returns the full object)DELETE /api/categories/{id}/- Delete category (used by transactions β 409)GET /api/categories/summary/- Category breakdown (?year=&month=optional, defaults to Jakarta current month)
GET /api/transactions/- List transactions (filters:category,category__type,date,search,ordering,page,year,month,type)POST /api/transactions/- Create transaction (amount must be > 0)GET /api/transactions/{id}/- Get transactionPATCH /api/transactions/{id}/- Update transaction (returns the full object)DELETE /api/transactions/{id}/- Delete transactionGET /api/transactions/summary/- Monthly summary (invalid year/month β 400)GET /api/transactions/trend/- 6-month trend (?months=1..24&year=&month=, zero-filled, Jakarta timezone)
These steps are a plan, not a record of a real deployment. Expect to adjust them.
- Logout semantics: logout blacklists the refresh token, but the access token stays valid until it expires (at most 60 minutes). This is accepted.
- Cookies are readable by JavaScript (XSS risk): access/refresh tokens are stored in regular cookies (
HttpOnlynot set) somiddleware.tscan see them. An XSS vulnerability would allow token theft. Recommended next step: HttpOnly cookies via Next.js route handlers (backend-for-frontend pattern). secureflag is production-only: a production build served over plain HTTP (e.g. a LAN IP) will not send cookies β production must use HTTPS.- Throttling cache is per-process (locmem): limits are per worker unless a shared cache (e.g. Redis) is configured.
- API docs are hidden in production unless
ENABLE_API_DOCS=1.
- π Reports - PDF/Excel export
- π° Budgeting - Monthly budget per category
- π Recurring Transactions - Automatic recurring entries
- π Multi-currency - Support multiple currencies
- π Notifications - Budget alerts, bill reminders
- πͺ HttpOnly cookies via BFF - Move token storage server-side
MIT License - feel free to use this project for personal or commercial purposes.