https://github.com/gonzalez8/fintrack
Personal investment tracker with FIFO portfolio engine, dividends, tax reports and monthly savings — Django + React + Celery + Docker
https://github.com/gonzalez8/fintrack
celery django docker drf nextjs postgresql react redis tailwindcss typescript
Last synced: about 2 months ago
JSON representation
Personal investment tracker with FIFO portfolio engine, dividends, tax reports and monthly savings — Django + React + Celery + Docker
- Host: GitHub
- URL: https://github.com/gonzalez8/fintrack
- Owner: Gonzalez8
- License: mit
- Created: 2026-02-19T23:08:58.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-05-15T18:01:02.000Z (about 2 months ago)
- Last Synced: 2026-05-15T20:27:17.530Z (about 2 months ago)
- Topics: celery, django, docker, drf, nextjs, postgresql, react, redis, tailwindcss, typescript
- Language: TypeScript
- Homepage: https://fintrack-phi-ten.vercel.app
- Size: 5.63 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: docs/CONTRIBUTING.md
- License: LICENSE
- Codeowners: CODEOWNERS
- Security: docs/SECURITY.md
Awesome Lists containing this project
README
# Fintrack
**Self-hosted investment portfolio tracker — built for privacy and clarity.**
Track your portfolio, transactions, dividends, interests and taxes from a single interface. No subscriptions, no data sharing, fully open source.
[](LICENSE)
[](https://github.com/Gonzalez8/fintrack/actions/workflows/ci.yml)
[](https://www.djangoproject.com/)
[](https://nextjs.org/)
[](https://react.dev/)
[](https://www.typescriptlang.org/)
[](https://www.docker.com/)
[](docs/CONTRIBUTING.md)
---
## Screenshots
---
## Features
### Portfolio Management
- **Multi-asset support** — Stocks, ETFs, Funds, Crypto
- **Real-time pricing** — Automatic updates via Yahoo Finance
- **Cost basis engines** — FIFO, LIFO, and Weighted Average Cost (WAC)
- **Position tracking** — Unrealized P&L, cost basis, market value per position
### Transaction Tracking
- **Buy / Sell / Gift** transactions with commission and tax support
- **Dividend tracking** — Gross, withholding tax, administration / custody fees, net and withholding rate per asset
- **Interest income** — Date-range based tracking per account, with optional explicit withholding tax (NULL = inferred from gross/net, 0 = confirmed no withholding) and fees
- **Import deduplication** — Hash-based duplicate detection
### Accounts & Snapshots
- **Multiple account types** — Operativa, Ahorro, Inversion, Depositos, Alternativos
- **Balance snapshots** — Historical balance tracking with auto-sync
- **Portfolio snapshots** — Periodic automated snapshots via Celery Beat
- **Bulk snapshot** — Update all account balances in one action
### Reports & Analytics
- **Year summary** — Dividends, interests, realized P&L, total income by year
- **Patrimonio evolution** — Total net worth over time (cash + investments)
- **Portfolio evolution** — Market value, cost basis, unrealized P&L charts
- **Monthly savings** — Cashflow and savings rate analysis
- **Annual savings** — Yearly savings aggregates and breakdown
- **Savings goals** — Target-based goals with 3-scenario projections (conservative, average, optimistic)
- **CSV exports** — Transactions, dividends, and interests
### Tax Reporting
Fintrack ships **two layers** of tax reporting:
#### Country-agnostic (always available)
- **Year summary** — Dividends, interests and realized P&L by year
- **Withholdings by country** — Foreign vs. local breakdown for any residency
- **CSV export** — Per-year transactions, dividends and interests
#### Country-specific tax-declaration assistant
- **Fiscal residence in Settings** — Pick your country (`Settings.tax_country`, ISO 3166-1 alpha-2). Default `ES`. The declaration tab only appears when an adapter exists for your country.
- **Spain (Modo Renta)** — Maps your data directly to the AEAT Renta Web boxes (Modelo 100):
- Interests, dividends (Spanish vs. foreign split), foreign double-taxation deduction (per-country, with the bilateral treaty cap — default 15%, configurable via `Settings.tax_treaty_limits`), capital gains
- Row-by-row declarable share sales with one-click "Copy row" (Entity / Transmission / Acquisition tab-separated)
- Final summary card with "Copy all" payload for the whole return
- Validation warnings — sales without cost basis, dividends without tax country, `gross ≠ net + withholding + fees` mismatches
- **Other countries** — Pluggable. Adding a country only touches its own adapter module. See [ADR-007: Per-Country Tax Adapter Pattern](docs/adr/007-tax-adapter-pattern.md) for the recipe and design rationale.
### Data Management
- **Snapshot scheduling** — Configurable auto-snapshot frequency (15min to 24h)
- **Data retention** — Automated purge of old snapshots (configurable: 1y, 5y, 10y, never)
- **Storage monitoring** — Per-user database space tracking
- **Bulk snapshots** — Update all account balances at once
- **JSON backup/restore** — Full data export and import
### Payroll Tracking
- **Digitised payslips** — Track monthly net, gross, Social Security contributions and income tax withheld per employer. Feeds the Spanish Modo Renta tab as "Rendimientos del trabajo".
- **Best-effort PDF parser (experimental)** — Upload a Spanish payslip PDF and Fintrack extracts gross, SS, IRPF, net, base IRPF and employer info to pre-fill the form. Suggestion-only, you always review and confirm before saving. Falls back to manual entry on unrecognised templates or scanned PDFs.
- **Employer registry** — Reusable employer records (name, CIF, SS account) referenced by every payslip. Inline employer creation from the payroll dialog.
- **Soft conciliation** — A `gross − SS − IRPF − net` warning surfaces when the identity breaks (anticipos, embargos, dietas exentas, retribución en especie…), but never blocks saving — real payslips legitimately diverge from that identity.
- See [ADR-008](docs/adr/008-payroll-and-pdf-parser.md) for the full design.
### Security & Auth
- **JWT in httpOnly cookies** — Access + refresh tokens, SameSite=Lax
- **Google OAuth 2.0** — One-click login with automatic account creation
- **Rate limiting** — Per-endpoint throttling (login, register, password change)
- **Multi-tenancy** — Strict owner-based data isolation
### Real Estate & Mortgage
- **Property tracking** — Current value, purchase price, net equity per property
- **Smart mortgage setup** — Enter loan amount, term (years), rate type (fixed/variable) and rate. Monthly payment, months paid, and outstanding balance auto-calculated from purchase date
- **Interactive amortization table** — Click any row (past or future) to add an early amortization. Choose reduce payment or reduce term per event. Schedule recalculates instantly
- **Multiple amortizations** — Add several events at different months, each cascading into the next
- **Timeline chart** — Balance curve (original vs modified), event markers, current position
- **Payment breakdown** — Principal vs interest stacked bars per month
- **Mortgage summary** — Side-by-side comparison: original vs modified for payment, installments, term, end date, total to pay, total interest, with savings highlighted
- **Amortization persistence** — Events stored in database, optimistic UI updates
### Privacy Mode
- **One-click toggle** — Eye icon in sidebar / mobile nav masks all personal monetary amounts
- **Smart masking** — Only personal amounts hidden (balances, gains, dividends). Public data stays visible: asset prices, percentages, share quantities
- **Persistent** — Stored in cookie, survives page reload
### Internationalization
- **5 languages** — Spanish, English, German, French, Italian
---
## Quick Start
### Option 1: Docker Compose (development)
```bash
git clone https://github.com/Gonzalez8/Fintrack.git && cd Fintrack
cp .env.example .env
docker compose up
```
| Service | URL |
|---|---|
| App | `http://localhost:3000` |
| API | `http://localhost:8000/api/` |
| Swagger UI | `http://localhost:8000/api/schema/swagger-ui/` |
| Django Admin | `http://localhost:8000/admin/` |
Create a superuser (optional):
```bash
docker compose exec backend python manage.py createsuperuser
```
### Option 2: Production (Pre-built Images)
No source code needed — uses images from GitHub Container Registry.
```bash
mkdir fintrack && cd fintrack
curl -O https://raw.githubusercontent.com/Gonzalez8/Fintrack/main/docker-compose.prod.yml
curl -O https://raw.githubusercontent.com/Gonzalez8/Fintrack/main/.env.production.example
cp .env.production.example .env
# Edit .env with your values (DB_PASSWORD, DJANGO_SECRET_KEY, etc.)
docker compose -f docker-compose.prod.yml up -d
```
The superuser is created automatically on first start (`admin` / `admin` by default — change in `.env`).
> Always set strong, unique values for `DB_PASSWORD`, `DJANGO_SECRET_KEY` and `DJANGO_SUPERUSER_PASSWORD` before deploying.
### Option 3: Portainer
1. In Portainer, go to **Stacks > Add stack**
2. Paste the contents of [`docker-compose.prod.yml`](docker-compose.prod.yml)
3. Add the required [environment variables](#environment-variables)
4. Click **Deploy the stack**
### Option 4: Live Demo (Vercel, with or without backend)
Enabling the demo flag adds a "Try Demo" button on the login page. Clicking it starts a [MSW (Mock Service Worker)](https://mswjs.io/) session with static data — no database needed. Real login and registration still work normally if a backend is configured.
| Vercel Setting | Value |
|---|---|
| Root Directory | `frontend` |
| Build Command | `npm run build` |
| Environment Variable | `NEXT_PUBLIC_DEMO_MODE=true` |
**Test demo locally:**
```bash
cd frontend && NEXT_PUBLIC_DEMO_MODE=true npm run dev
```
> `NEXT_PUBLIC_DEMO_MODE=true` enables the demo button — it does **not** replace real auth. Demo sessions are identified by a `demo: true` flag in the JWT token, so they coexist with real users on the same deployment.
---
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Browser │
└──────────────────────────┬──────────────────────────────────┘
│
┌────────────▼────────────┐
│ Nginx (port 80) │ Reverse proxy
└──┬──────────────────┬───┘
│ │
/api/* │ /admin/* /static/* │ /*
┌──────▼──────┐ ┌──────▼──────┐
│ Django 5.1 │ │ Next.js 16 │
│ Gunicorn │ │ SSR + BFF │
│ Port 8000 │ │ Port 3000 │
└──┬─────┬───┘ └─────────────┘
│ │
┌────────▼┐ ┌─▼────────┐
│ Postgres │ │ Redis │
│ :5432 │ │ :6379 │
└──────────┘ └─┬────┬───┘
│ │
┌─────────▼┐ ┌─▼──────────┐
│ Celery │ │ Celery Beat │
│ Worker │ │ Scheduler │
└──────────┘ └─────────────┘
```
### BFF Pattern
The browser **never** calls the Django API directly. All requests flow through Next.js Route Handlers (`/api/proxy/*`), which:
1. Read JWT from httpOnly cookies
2. Forward the request to Django with the Authorization header
3. Handle token refresh transparently
4. Return the response to the browser
### Rendering Strategy
| Route Group | Strategy | Description |
| -------------- | ------------------ | ---------------------------------- |
| `(marketing)/` | SSG | Landing page, static generation |
| `(auth)/` | Client Components | Login, Register forms |
| `(dashboard)/` | SSR + Streaming | Server Components + React Suspense |
---
## Security & Authentication
### JWT token flow
```
Browser
├── Cookie ──► access token (httpOnly, SameSite=Lax)
└── Cookie ──► refresh token (httpOnly, SameSite=Lax)
│
└─► POST /api/auth/token/refresh/ ──► new access token
```
- Refresh token rotates on every use and is blacklisted after rotation (replay-safe).
- All requests attach Bearer token automatically; 401 triggers a single transparent retry.
### Google OAuth2
```
1. Load Google GIS script on login page
2. User clicks "Continue with Google"
3. Google returns an ID token (credential)
4. POST /api/auth/google/ { credential }
5. Backend verifies token with Google's public keys
6. Get or create user by email
7. Issue JWT pair → login complete
```
**Setup:**
1. [Create an OAuth 2.0 Client ID](https://console.cloud.google.com/apis/credentials) (Web application)
2. Add your domain to **Authorized JavaScript origins**
3. Set `GOOGLE_CLIENT_ID=` in `.env`
---
## Tech Stack
| Layer | Technology |
|---|---|
| **Backend** | Django 5.1 · Django REST Framework 3.15 · PostgreSQL 16 · Celery 5.3 · Redis 7 |
| **Auth** | djangorestframework-simplejwt · google-auth |
| **Frontend** | Next.js 16 · React 19 · TypeScript 5 · Tailwind CSS v4 · shadcn/ui (Base UI) |
| **State** | TanStack Query 5 (server state) |
| **Charts** | Recharts 3 · Lightweight Charts 5 |
| **i18n** | Custom i18n (es, en, de, fr, it) |
| **Infra** | Docker Compose · GitHub Actions CI · GHCR |
| **API Docs** | drf-spectacular (OpenAPI 3 / Swagger UI) |
---
## Project Structure
```
fintrack/
├── backend/ Django 5.1 + DRF
│ ├── apps/
│ │ ├── core/ JWT auth, base models, health check
│ │ ├── assets/ Assets, accounts, snapshots, settings
│ │ ├── transactions/ Transaction, Dividend, Interest
│ │ ├── portfolio/ Cost basis engine (FIFO/LIFO/WAC)
│ │ ├── reports/ Tax summaries, evolution, savings, goals
│ │ ├── realestate/ Property, Amortization, mortgage simulation
│ │ └── importer/ JSON backup / restore
│ └── config/
│ ├── settings/ base.py · development.py · production.py
│ ├── urls.py
│ └── celery.py
│
├── frontend/ Next.js 16 App Router
│ └── src/
│ ├── app/
│ │ ├── (marketing)/ Landing page (SSG)
│ │ ├── (auth)/ Login, Register (Client Components)
│ │ ├── (dashboard)/ Protected pages (SSR + Streaming)
│ │ └── api/ BFF Route Handlers (proxy to Django)
│ ├── components/
│ │ ├── ui/ shadcn/ui (Base UI + Tailwind v4)
│ │ └── app/ Domain-specific components
│ ├── lib/ api-client, utils, mortgage-math, privacy
│ ├── types/ TypeScript interfaces
│ ├── demo/ MSW handlers (demo mode)
│ └── i18n/ Translations (es, en, de, fr, it)
│
├── .github/workflows/
│ ├── ci.yml Backend tests + frontend typecheck
│ └── docker-publish.yml Build & push to GHCR
├── docker-compose.yml Development (6 services)
├── docker-compose.prod.yml Production (pre-built images from GHCR)
└── .env.example Environment variable template
```
---
## API
Interactive docs available at [`/api/schema/swagger-ui/`](http://localhost:8000/api/schema/swagger-ui/) when running locally.
```
# Auth
POST /api/auth/token/ Login → { access, user } + httpOnly cookie
POST /api/auth/token/refresh/ Rotate refresh token → { access }
POST /api/auth/logout/ Blacklist token + clear cookie
GET /api/auth/me/ Current user
POST /api/auth/register/ Create account (403 if disabled)
POST /api/auth/google/ Google ID-token login / register
GET /api/auth/profile/ User profile
PUT /api/auth/profile/ Update username / email
POST /api/auth/change-password/ Change password + rotate JWT
# Data (all owner-scoped, require Bearer token)
CRUD /api/assets/
POST /api/assets/update-prices/ Enqueue → 202 { task_id }
GET /api/tasks/{task_id}/ Celery task status
CRUD /api/accounts/
CRUD /api/account-snapshots/
POST /api/accounts/bulk-snapshot/
GET/PUT /api/settings/
CRUD /api/transactions/
CRUD /api/dividends/
CRUD /api/interests/
GET /api/portfolio/
GET /api/reports/year-summary/
GET /api/reports/patrimonio-evolution/
GET /api/reports/rv-evolution/
GET /api/reports/monthly-savings/
GET /api/reports/annual-savings/
GET /api/reports/snapshot-status/
GET /api/reports/tax-declaration/?year=YYYY Renta Web payload (Modo Renta)
CRUD /api/savings-goals/
GET /api/savings-goals/{id}/projection/ Goal progress projection
CRUD /api/properties/ Real estate properties
POST /api/properties/simulate/ Mortgage amortization simulation
CRUD /api/amortizations/ Early amortization events (?property=uuid)
GET /api/storage-info/ Database space usage
GET /api/export/transactions.csv
GET /api/export/dividends.csv
GET /api/export/interests.csv
GET /api/backup/export/
POST /api/backup/import/
GET /api/health/ Liveness probe
```
---
## Environment Variables
### Backend (`.env`)
| Variable | Required | Default | Description |
|---|---|---|---|
| `DB_PASSWORD` | **yes** | — | PostgreSQL password |
| `DJANGO_SECRET_KEY` | **yes** | — | Django secret key |
| `DB_NAME` | | `fintrack` | Database name |
| `DB_USER` | | `fintrack` | Database user |
| `ALLOWED_HOSTS` | | `*` | Comma-separated allowed hostnames. `backend` is added automatically for internal Docker communication. |
| `CORS_ALLOWED_ORIGINS` | | — | Comma-separated allowed origins (e.g., `https://fintrack.example.com`) |
| `CSRF_TRUSTED_ORIGINS` | | same as CORS | Comma-separated CSRF trusted origins (e.g., `https://fintrack.example.com`) |
| `REDIS_URL` | | `redis://redis:6379/0` | Celery broker + result backend |
| `GOOGLE_CLIENT_ID` | | _(empty)_ | Google OAuth2 client ID |
| `ALLOW_REGISTRATION` | | `true` | `false` = admin creates users via Django admin |
| `DJANGO_SUPERUSER_USERNAME` | | `admin` | Initial superuser username |
| `DJANGO_SUPERUSER_PASSWORD` | | `admin` | Initial superuser password |
| `DJANGO_SUPERUSER_EMAIL` | | `admin@fintrack.local` | Initial superuser email |
| `APP_PORT` | | `8080` | Host port for frontend (prod) |
### Frontend
| Variable | Default | Description |
|---|---|---|
| `NEXT_PUBLIC_API_URL` | `http://localhost:8000` | Public API URL |
| `NEXT_PUBLIC_DEMO_MODE` | `false` | `true` shows "Try Demo" button (coexists with real auth) |
| `DJANGO_INTERNAL_URL` | `http://backend:8000` | Internal Django URL (SSR) |
---
## Development
### Common commands
```bash
# Start all services
docker compose up
# Run backend tests
docker compose exec backend pytest
# TypeScript check
docker compose exec frontend npx tsc --noEmit
# Create migrations
docker compose exec backend python manage.py makemigrations
# Django shell
docker compose exec backend python manage.py shell
# View Celery logs
docker compose logs -f celery_worker
```
### Celery tasks
| Task | Schedule | Description |
|---|---|---|
| `snapshot_all_users_task` | Every 60s | Create portfolio snapshots when due |
| `purge_old_snapshots_task` | Daily | Delete snapshots past retention period |
| `update_prices_task` | On-demand | Fetch prices from Yahoo Finance |
---
## Production Deployment
### Reverse proxy setup (Nginx Proxy Manager)
Point your domain to the frontend container:
| Setting | Value |
|---|---|
| **Domain** | `fintrack.example.com` |
| **Scheme** | `http` |
| **Forward Hostname/IP** | server local IP (e.g., `192.168.1.171`) |
| **Forward Port** | `8080` (or your `APP_PORT`) |
| **Websockets Support** | enabled |
| **SSL** | Let's Encrypt |
The Django admin panel is available at `http://:8001/admin/` (add the IP to `ALLOWED_HOSTS`).
### Startup sequence
The backend container runs automatically on each start:
1. `migrate` — apply database migrations
2. `collectstatic` — collect static files
3. `createsuperuser` — create admin user (skips if already exists)
4. `gunicorn` — start the application server
### Updating
After pushing to `main`, GitHub Actions rebuilds and publishes Docker images to GHCR.
- **Portainer**: Stack → Editor → "Update the stack" with "Re-pull image" enabled
- **CLI**: `docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d`
### Security settings (automatic when `DEBUG=False`)
- `JWT_AUTH_COOKIE_SECURE = True`
- `SECURE_HSTS_SECONDS = 31536000` (1 year)
- `SECURE_SSL_REDIRECT = True`
- `SESSION_COOKIE_SECURE = True`
- `CSRF_COOKIE_SECURE = True`
### Checklist
- [ ] Set a strong `DJANGO_SECRET_KEY` (50+ random chars)
- [ ] Set `DB_PASSWORD` to a secure value
- [ ] Configure `ALLOWED_HOSTS` and `CORS_ALLOWED_ORIGINS`
- [ ] Set `CSRF_TRUSTED_ORIGINS` for your domain (e.g., `https://fintrack.example.com`)
- [ ] Configure `GOOGLE_CLIENT_ID` if using Google login
- [ ] Set `ALLOW_REGISTRATION=false` if single-user
- [ ] Set up SSL termination (NPM, Caddy, Traefik, or cloud LB)
- [ ] Configure backup strategy for PostgreSQL
- [ ] Change `DJANGO_SUPERUSER_PASSWORD`
### Database Backups
```bash
# Manual backup (compressed)
docker compose -f docker-compose.prod.yml exec -T db pg_dump -U fintrack fintrack | gzip > backups/fintrack_$(date +%Y%m%d).sql.gz
# Restore from backup
gunzip -c backups/fintrack_20260315.sql.gz | docker compose -f docker-compose.prod.yml exec -T db psql -U fintrack fintrack
```
**Automated daily backup with 30-day retention** — add to your server's crontab (`crontab -e`):
```
0 3 * * * cd /path/to/fintrack && mkdir -p backups && docker compose -f docker-compose.prod.yml exec -T db pg_dump -U fintrack fintrack | gzip > backups/fintrack_$(date +\%Y\%m\%d).sql.gz && find backups/ -name "fintrack_*.sql.gz" -mtime +30 -delete
```
This runs at 3:00 AM daily, creates a compressed dump (~10-50 MB), and deletes backups older than 30 days.
### Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| `Bad Request (400)` | Domain/IP not in `ALLOWED_HOSTS` | Add it to `ALLOWED_HOSTS` env var |
| `400` on login (POST) | `CSRF_TRUSTED_ORIGINS` not set | Set to `https://your-domain.com` |
| `ECONNREFUSED` on login | Backend not ready | Wait — healthcheck has 30s start period |
| `502 Bad Gateway` | Backend crashed | Check logs: `docker logs fintrack-backend-1` |
| Admin login fails | Superuser not created | Verify `DJANGO_SUPERUSER_*` env vars are set |
---
## Documentation
- [Development Guide](docs/DEVELOPMENT.md) — Setup, testing, linting, common tasks
- [Architecture](docs/architecture.md) — C4 diagrams, system design, ADRs
- [Security Policy](docs/SECURITY.md) — Vulnerability reporting, security measures, backups
- [Contributing Guide](docs/CONTRIBUTING.md) — Branch naming, commit conventions, PR requirements
- [Changelog](CHANGELOG.md) — Version history and notable changes
## Contributing
See [CONTRIBUTING.md](docs/CONTRIBUTING.md) for the full guide. Quick summary:
1. Fork the repository
2. Create a feature branch (`git checkout -b feat/amazing-feature`)
3. Write tests for your changes
4. Ensure all CI checks pass (`lint`, `typecheck`, `tests`)
5. Commit using [Conventional Commits](https://www.conventionalcommits.org/)
6. Push and open a Pull Request
### Conventions
- **UI labels** use i18n (es, en, de, fr, it), **code** (variables, comments) in English
- **Money**: Always `Decimal`, never `float`
- **IDs**: UUID via `TimeStampedModel`
- **Multi-tenancy**: Every model has `owner` FK, ViewSets use `OwnedByUserMixin`
- **BFF Pattern**: Browser → Next.js → Django (browser never calls Django directly)
---
## License
[MIT](LICENSE) — free to use, modify and self-host.