Investigate your finances. Track smarter. Spend wiser.
Finvestigate is a multi-user personal finance tracker built with Python + Flask and PostgreSQL. Users sign up, log transactions (income/expense), browse and filter their transaction history, and see a monthly dashboard with a category-wise spending breakdown chart.
Note on this README: The repository's original README only described the project as "planned" / "under active development" with a TBD database and frontend. That's outdated β the codebase is significantly further along than the README suggests. This document reflects what is actually implemented in code, not the aspirational feature list.
- What This App Actually Is
- Tech Stack
- Data Model
- Project Structure
- Routes / Endpoints
- Getting Started (Local Dev)
- Environment Variables
- Running with Docker
- Database Seeding
- Testing
- CI/CD
- Deployment
- Known Gaps / Things to Improve
- Color Scheme
- License
A server-rendered Flask web app (not an API-only backend, not a SPA) where each authenticated user manages their own private set of financial data:
- Auth: email/password signup & login, hashed passwords, session-based auth via Flask-Login
- Role-based access control: every user has a role (
adminoruser) via arolestable - Transactions: create, edit, delete income/expense entries with a title, amount, category, date, and optional note
- Transaction browsing: paginated list (10/page), filterable by type/category/month, searchable by title/note
- Dashboard: current month's income, expenses, balance, 5 most recent transactions, and a donut chart (Chart.js) of expenses by category
- Categories: system-seeded defaults (5 income, 7 expense β see Database Seeding), each with an emoji icon
- Budgets (data model exists, UI/routes not yet built β see Known Gaps): monthly budget per category with an alert threshold
- Audit log (data model exists, not yet wired into routes): intended to track user actions with IP/module
- Multi-currency preference per user (defaults to
PKR), stored but not yet used for conversion β it's a display label only - Static pages: Home, About, How to Use, Contact
- Custom error pages: 403, 404, 500
In short: it's a CRUD-heavy personal finance tracker, further along than a starter template, but with a few modeled features (budgets, audit logging, real currency conversion) not yet exposed through routes/UI.
| Layer | Technology | Notes |
|---|---|---|
| Language | Python 3.11+ | |
| Web framework | Flask 3.1.3 | Blueprint-based (main, auth) |
| ORM | SQLAlchemy 2.0 (via Flask-SQLAlchemy 3.1.1) | |
| Database | PostgreSQL 15 | via psycopg2-binary |
| Migrations | Flask-Migrate 4.1.0 (Alembic) | |
| Auth | Flask-Login 0.6.3 | session-based |
| Password hashing | Werkzeug security helpers | generate_password_hash / check_password_hash |
| Flask-Mail 0.10.0 | configured but not yet used in any route | |
| Frontend templating | Jinja2 | server-rendered HTML |
| CSS | Tailwind CSS | loaded via CDN (cdn.tailwindcss.com), not a build pipeline |
| Charts | Chart.js | loaded via CDN, donut chart on dashboard |
| Config/env | python-dotenv | .env / .env.production |
| Testing | pytest | tests/test_routes.py |
| Containerization | Docker (multi-stage build), Docker Compose | non-root user, healthcheck |
| CI | GitHub Actions β ci-pipeline.yml |
flake8 lint, bandit security scan |
| CD | GitHub Actions β cd-pipeline.yml (triggered via workflow_run after CI) |
hadolint (Dockerfile lint), pip-audit (dependency scan) |
| Alt. deploy path | Azure Pipelines (azure-pipelines-deploy.yml, azure-pipelines-teardown.yml) |
separate/parallel deploy option |
| Secret scanning | Gitleaks (.gitleaks.toml) |
Six tables, defined in app/models.py:
Role (roles)
ββ id, name [admin|user], description, created_at
User (users)
ββ id, full_name, username (unique), email (unique), password (hashed),
is_active, is_verified, currency_preference (default "PKR"),
role_id β roles.id, created_at, updated_at, last_login
Category (categories)
ββ id, name, type [income|expense], icon (emoji), is_default,
user_id β users.id (nullable β null = system default category)
Transaction (transactions)
ββ id, title, amount (Numeric 12,2), type [income|expense], note, date,
user_id β users.id, category_id β categories.id, created_at, updated_at
Budget (budgets)
ββ id, title, amount (Numeric 12,2), month, year, alert_at (default 80),
user_id β users.id, category_id β categories.id, created_at
AuditLog (audit_logs)
ββ id, action, module, ip_address, user_id β users.id, created_at
Relationships: a User has many Transaction, Budget, and AuditLog rows (cascade delete). A Transaction and Budget optionally belong to a Category.
finvestigate/
βββ .github/workflows/
β βββ ci-pipeline.yml # lint (flake8) + security scan (bandit)
β βββ cd-pipeline.yml # runs after CI: hadolint + pip-audit
βββ app/
β βββ __init__.py # app factory, extension init, auto-seeding, error handlers
β βββ auth.py # signup/login/logout blueprint
β βββ config.py # Config / DevelopmentConfig / ProductionConfig / TestingConfig
β βββ models.py # SQLAlchemy models (6 tables, see above)
β βββ routes.py # main blueprint: dashboard, transactions CRUD, static pages
β βββ seed.py # standalone seed script (roles + default categories)
β βββ templates/
β βββ base.html # layout, Tailwind CDN, nav
β βββ home.html, about.html, contact.html, how_to_use.html
β βββ dashboard.html # summary cards + Chart.js donut chart
β βββ transactions.html # paginated/filterable transaction table
β βββ auth/login.html, auth/signup.html
β βββ errors/403.html, 404.html, 500.html
βββ tests/
β βββ test_routes.py # smoke tests: public routes 200, protected routes redirect (302)
βββ azure-pipelines-deploy.yml
βββ azure-pipelines-teardown.yml
βββ Dockerfile # multi-stage: builder β slim production image
βββ docker-compose.yml # web (Flask) + db (Postgres 15-alpine)
βββ deploy.sh # server-side deploy script (git pull + docker compose up), invoked via SSH from CI
βββ entrypoint.sh # waits for DB, runs migrations, starts Flask
βββ requirements.txt
βββ run.py # entrypoint: creates app via factory, runs dev server
βββ .gitleaks.toml # secret-scanning config
Public
| Method | Path | Description |
|---|---|---|
| GET | / |
Home page |
| GET | /about |
About page |
| GET | /how-to-use |
How-to-use page |
| GET | /contact |
Contact page |
| GET, POST | /signup |
Create account |
| GET, POST | /login |
Log in |
| POST | /logout |
Log out |
Authenticated (require login)
| Method | Path | Description |
|---|---|---|
| GET | /dashboard |
Monthly summary + recent transactions + category chart |
| GET | /transactions |
Paginated, filterable, searchable transaction list |
| POST | /dashboard/add-transaction |
Create a transaction |
| POST | /transactions/edit/<id> |
Edit a transaction (owner-only, 404 otherwise) |
| POST | /transactions/delete/<id> |
Delete a transaction (owner-only, 404 otherwise) |
Budget and audit-log routes are not yet implemented despite the models existing.
- Python 3.11+
- PostgreSQL 15 (local install or Docker)
- pip / virtualenv
# 1. Clone
git clone https://github.com/mananurrehman/finvestigate.git
cd finvestigate
# 2. Create & activate a virtual environment
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 3. Install dependencies
pip install -r requirements.txt
# 4. Create a .env file from the provided example
cp .env.example .env
# then edit .env with your real values
# 5. Make sure PostgreSQL is running and the database from .env exists
# 6. Run the app (auto-creates tables + seeds roles/categories on first run)
python run.pyApp will be available at http://localhost:5000.
See .env.example for the full list with comments. Summary:
# Flask
SECRET_KEY=change-me-in-production
# Database β either provide DATABASE_URL directly, or the individual parts below
DATABASE_URL=postgresql://user:password@localhost:5432/finvestigate
DB_USER=
DB_PASSWORD=
DB_HOST=localhost
DB_PORT=5432
DB_NAME=finvestigate
# Mail (Flask-Mail β configured, not yet used by any route)
MAIL_SERVER=
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_DEFAULT_SENDER=Note:
app/__init__.pybuilds the DB URI fromDB_USER/DB_PASSWORD/DB_HOST/DB_PORT/DB_NAMEdirectly and does not checkDATABASE_URLfirst, whileapp/config.py'sConfig.get_db_uri()does preferDATABASE_URL. These two are inconsistent βconfig.py's config classes aren't actually wired intocreate_app()at all right now. Set bothDATABASE_URLand the individualDB_*vars in your.envto be safe until this is fixed (see Known Gaps).
# Build and run the full stack (Flask + Postgres) via Docker Compose
docker compose --env-file .env.production up --buildwebservice exposes the app on host port5002β container port5000dbservice runs Postgres 15-alpine with a persistent volume bound to/home/ubuntu/finvestigate-data/postgres(this is a hardcoded path meant for the production server β override for local use)entrypoint.shwaits for the DB, runs migrations (ordb.create_all()if nomigrations/folder exists yet), then starts Flask
Seeding happens automatically every time the app starts (seed_defaults() in app/__init__.py), and is idempotent (checks .count() == 0 first). There's also a standalone script for manual seeding:
python -m app.seedDefault roles: admin, user
Default income categories: Salary πΌ, Freelance π», Investment π, Forex Trading πΉ, Other Income π°
Default expense categories: Food π, Transport π, Shopping ποΈ, Bills π, Health π₯, Education π, Other π¦
pytestCurrent coverage (tests/test_routes.py) is minimal β smoke tests only:
- Public pages return
200 - Protected routes (
/dashboard,/transactions) redirect (302) when not logged in
No tests currently cover auth flows, transaction CRUD, filtering, or model validation.
CI (ci-pipeline.yml) β triggers on push to dev:
flake8lint (non-blocking β warnings logged, doesn't fail the build)banditsecurity scan
CD (cd-pipeline.yml) β triggers after CI completes successfully:
hadolintβ Dockerfile lintpip-auditβ dependency vulnerability scan- (presumably a deploy job calling
deploy.shover SSH, matching the script's comment "Called by GitHub Actions Workflow via SSH")
Alternative pipeline: azure-pipelines-deploy.yml / azure-pipelines-teardown.yml provide an Azure DevOps-based deploy/teardown path as an alternative to the GitHub Actions + SSH route.
The app is designed for a self-managed VM, not serverless:
deploy.shruns on the target server (via SSH from CI): clones/pulls the repo, tears down old containers, rebuilds, and brings the stack up withdocker compose- Persistent Postgres data is bound to a fixed host path (
/home/ubuntu/finvestigate-data/postgres) - The Dockerfile is a multi-stage build: a
builderstage installs dependencies, and the final slim image copies only the installed packages + app code, runs as a non-rootfinvestigateuser, and defines aHEALTHCHECKhitting/
This section is here deliberately β the repo isn't "perfect," and a new contributor should know where the rough edges are:
- README was stale β described the app as feature-incomplete when the codebase is much further along (this file replaces that).
- No
.env.examplewas previously committed β this has been added alongside this README to fix that. - Config duplication/inconsistency β
app/config.pydefinesDevelopmentConfig/ProductionConfig/TestingConfig, butcreate_app()inapp/__init__.pybuilds its own config inline instead of using them. Pick one source of truth. - Budgets model has no routes/UI β schema exists (
Budgettable,alert_atthreshold) but nothing inroutes.pyreads or writes it yet. - Audit log model has no writers β
AuditLogtable exists but no route currently creates a log entry. - Flask-Mail is configured but unused β no password-reset, email verification, or notification flow uses it yet, despite
is_verifiedexisting on theUsermodel. currency_preferenceis cosmetic only β no actual currency conversion; it's just a label.- Tailwind via CDN β fine for prototyping, but no purge/build step means larger payloads and no custom design tokens; a proper Tailwind build (PostCSS/CLI) would be more production-appropriate.
- Test coverage is minimal β only smoke tests for route status codes; no coverage of business logic (transaction totals, filters, pagination, budget alerts).
flake8in CI is non-blocking (|| true) β lint issues are surfaced but never fail the build.- Hardcoded deployment path in
docker-compose.yml(/home/ubuntu/finvestigate-data/postgres) β not portable to other environments without editing the compose file.
Primary: #6366f1 (Indigo)
Secondary: #8b5cf6 (Purple)
Accent: #06b6d4 (Cyan)
Success: #10b981 (Emerald)
Warning: #f59e0b (Amber)
Danger: #ef4444 (Red)
Light Mode:
Background: #f8fafc
Surface: #ffffff
Text: #1e293b
Muted: #64748b
Dark Mode:
Background: #0f172a
Surface: #1e293b
Text: #f1f5f9
Muted: #94a3b8
MIT License