A modern, dockerized expense tracking application built with Flask, SQLAlchemy, and Chart.js.
I was originally using expenseowl but there was some functionality I needed that was missing. Since it doesn't seem to be actively developed anymore, I decided to brush off my web-dev skills and make my own. This project is also my first try at using LLMs to assist with coding.
- β¨ Real-time Updates: Add, edit, and delete transactions without page refreshes
- π Visual Analytics: Interactive charts with income/expense tracking and category/tag filtering
- π° Income & Expense Tracking: Track both income and expenses with net balance calculation
- π·οΈ Flexible Tagging System: Add custom tags to transactions for fine-grained organization
- π Category Management: Create, rename, merge, and delete categories from the Settings page
- π Tag Management: Rename, merge, and delete tags across all transactions from the Settings page
- π Multi-select Filtering: Filter by multiple categories and/or tags simultaneously
- π Time-range Filtering: Filter by current month, custom date range, or all time
- π Month-based Pagination: Transactions are grouped and paginated by month
- π³ Dockerized: Easy deployment with Docker and Docker Compose
- πΎ Multiple Database Backends: SQLite, PostgreSQL, or MySQL
- π€ CSV Import: Bulk import transactions from a CSV file
- π₯ CSV Export: Download your full transaction history (or a filtered date range) as a CSV file
- π§ͺ Test Data Generator: Populate with sample data (debug mode only)
- ποΈ SOLID Architecture: Clean separation of concerns across models, services, and routes
- π± Mobile Responsive: Works on all device sizes
- Backend: Flask (Python 3.11+) with SQLAlchemy ORM
- Frontend: Vanilla JS with fetch-based dynamic updates
- Charts: Chart.js
- Database: SQLite (default), PostgreSQL, or MySQL
- Build: flit / pyproject.toml
- Containerization: Docker & Docker Compose
The application supports three database backends. SQLite requires no extra services and is the default.
SQLite (default):
docker-compose up --buildPostgreSQL:
docker-compose -f docker-compose.postgres.yml up --buildMySQL:
docker-compose -f docker-compose.mysql.yml up --buildThen open http://localhost:5000 in your browser.
# Install the package in editable mode (includes all dependencies)
pip install -e .
# Initialise the database
flask --app JustAnotherExpenseManager init-db
# Run the development server
flask --app JustAnotherExpenseManager runOr use the installed entry point directly:
JustAnotherExpenseManagerThen open http://localhost:5000.
All configuration is done through environment variables. Copy .env.example to .flaskenv for local development and edit as needed.
# Bind to localhost only (default for local dev β more secure)
FLASK_RUN_HOST=127.0.0.1
# Bind to all interfaces (required for Docker)
FLASK_RUN_HOST=0.0.0.0
FLASK_RUN_PORT=5000The Docker images set FLASK_RUN_HOST=0.0.0.0 automatically via ENV in the Dockerfile. You can override it in your docker-compose.yml if needed.
# SQLite (default)
DATABASE_TYPE=sqlite
SQLITE_PATH=./data/expenses.db
# PostgreSQL
DATABASE_TYPE=postgresql
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=expenses
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
# MySQL
DATABASE_TYPE=mysql
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_NAME=expenses
DATABASE_USER=expenses_user
DATABASE_PASSWORD=expenses_passSee DATABASE_GUIDE.md for full configuration details and migration guides.
The application has three pages:
- Summary: Financial overview, charts, and multi-select category/tag/time filtering
- Transactions: Add, edit, delete, and filter individual transactions; CSV import
- Settings: Manage categories and tags; export transactions as CSV; test data generator (debug mode only)
- Navigate to the Transactions page
- Fill in the form:
- Description: What the transaction was for
- Amount: Positive number
- Type: Income or Expense
- Date: When it occurred
- Category: Choose from existing or add new ones in Settings
- Tags: Optional comma-separated tags (e.g.
recurring, business)
- Click Add Transaction β it appears immediately in the list
Both the Summary and Transactions pages support combined filtering:
- Category filter: Multi-select β choose one or more categories (or All)
- Tag filter: Multi-select β choose one or more tags (or All Tags)
- Time range: Current month, custom date range, or all time
Filters are reflected in the URL as query parameters (categories=, tags=, range=/start_date=/end_date=), so filtered views can be bookmarked or shared.
Click the Edit button on any transaction row to open an inline edit form. Changes are saved immediately without a page reload.
Go to Settings β Category Management to:
- Add new categories
- Rename a category (updates all transactions that use it)
- Merge one category into another (all transactions from the source are re-assigned to the target)
- Delete a category (removes it from all transactions)
Go to Settings β Tag Management to:
- Rename a tag across all transactions
- Merge one tag into another
- Delete a tag from all transactions
Go to Settings β Export Transactions to download your transaction history as a CSV file.
- Leave both date fields blank to export all transactions
- Fill in Start Date and/or End Date to export a specific date range
- The exported file uses the same column format as CSV import, so it can be edited and re-imported
The export endpoint (GET /api/transactions/export) also accepts categories, tags, range, start_date, and end_date query parameters, matching the filter interface on the Transactions and Summary pages.
- Prepare a CSV file with these columns:
| Column | Description |
|---|---|
description |
What the transaction was for |
amount |
Positive dollar amount |
type |
income or expense |
category |
Category name (no category: prefix) |
date |
YYYY-MM-DD format |
tags |
Optional, comma-separated |
- Go to the Transactions page and use the Import from CSV section
- Invalid rows are reported individually; valid rows are still imported
Example:
description,amount,type,category,date,tags
Monthly Salary,5000.00,income,salary,2024-01-15,recurring
Grocery Store,125.50,expense,food,2024-01-16,
Freelance Project,1500.00,income,freelance,2024-01-20,one-timeJustAnotherExpenseManager/ # Main Python package
βββ __init__.py # App factory (create_app) and CLI commands
βββ models/
β βββ __init__.py # SQLAlchemy models: Transaction, Tag
βββ routes/
β βββ transactions.py # Transaction CRUD, CSV import, and CSV export endpoints
β βββ stats.py # Summary page and /api/stats, /api/chart-data
β βββ categories.py # Category and tag management endpoints
β βββ settings.py # Settings page and test data endpoints
βββ utils/
β βββ database.py # DatabaseManager β connections and init
β βββ services.py # Business logic: TransactionService, StatsService, etc.
βββ templates/
β βββ base.html # Base layout with navigation
β βββ summary.html # Summary page
β βββ transactions.html # Transactions page
β βββ settings.html # Settings page
β βββ stats.html # Stats partial (rendered by /api/stats)
β βββ transactions_list.html # Transactions list partial with pagination
βββ static/
βββ css/styles.css
βββ js/
βββ filter_component.js # Multi-select category/tag filter UI
βββ stats.js # Chart initialisation and stats refresh
βββ transactions.js # Transaction form, edit modal, delete
βββ settings.js # Category/tag management and CSV export UI
pyproject.toml # Package metadata and dependencies (flit)
.flaskenv # Local dev environment variables
.env.example # Environment variable reference
Dockerfile # Container image definition
docker-compose.yml # SQLite deployment
docker-compose.postgres.yml # PostgreSQL deployment
docker-compose.mysql.yml # MySQL deployment
DATABASE_GUIDE.md # Detailed database configuration guide
Models (models/) β SQLAlchemy table definitions and ORM relationships.
Services (utils/services.py) β All business logic: transaction CRUD, statistics calculations, category/tag operations, test data generation. Routes call services; services never touch HTTP.
Routes (routes/) β Flask blueprints that handle HTTP requests, validate input, call services, and return responses. No business logic lives here.
Database (utils/database.py) β DatabaseManager wraps engine creation, session factory, and init_database / reset_database lifecycle methods.
# Install with test dependencies
pip install -e ".[test]"
# Run all tests
pytest
# With coverage report
pytest --cov=JustAnotherExpenseManager --cov-report=term-missing# Install Node dependencies and browsers
npm ci
npx playwright install --with-deps chromium
# Run all E2E tests (starts the dev server automatically)
npx playwright test
# Run a specific browser only
npx playwright test --project=chromium
# Open the interactive UI runner
npx playwright test --ui
# View the last HTML report
npx playwright show-reportE2E tests require the application to be runnable via JustAnotherExpenseManager (the installed entry point). Playwright's webServer config in playwright.config.js handles starting and stopping it automatically.
# Initialise tables (run once after install)
flask --app JustAnotherExpenseManager init-db
# Drop and recreate all tables
flask --app JustAnotherExpenseManager init-db --drop
# Load sample data after init
flask --app JustAnotherExpenseManager init-db --sample-data
# Check database connectivity
flask --app JustAnotherExpenseManager db-health
# Show database info
flask --app JustAnotherExpenseManager db-infoMIT β see LICENSE for details.