A multi-tracker personal finance API built with FastAPI, SQLModel, and PostgreSQL.
Author: Dipto Karmakar
License: AGPL-3.0 — free to study, not free to commercialize
Spendrift is a personal finance backend where each user can manage multiple Trackers (e.g. a "Bangladesh Tracker" in BDT, a "Europe Tracker" in EUR). Every tracker is an independent workspace with its own expenses, categories, and budgets.
- FastAPI — async REST API
- SQLModel + Alembic — ORM and migrations
- PostgreSQL 18 — primary database
- Pydantic v2 — request/response validation
- Argon2 + JWT — authentication
- SlowAPI — rate limiting
- Docker — local development
| Module | Status |
|---|---|
| Auth | Complete |
| Users | Complete |
| Trackers | Complete |
| Categories | Complete |
| Expenses | Complete |
| Budgets | Complete |
| Dashboard | Complete |
| Reports | Complete |
- Docker (for PostgreSQL)
- Python 3.11+
uvpackage manager- S3-compatible object storage (Cloudflare R2, MinIO, AWS S3) for avatar uploads
# 1. Start PostgreSQL
docker-compose up -d
# 2. Install dependencies
pip install -e ".[postgres]"
# 3. Copy and configure environment
cp .env.example .env
# Edit .env with your DATABASE_URL, SECRET_KEY, and STORAGE_* credentials
# 4. Apply migrations
make upgrade
# 5. Start the API
make runAPI runs at http://localhost:8000
Interactive docs at http://localhost:8000/docs
make run # Start API with hot reload (port 8000)
make migrations # Generate Alembic migration (prompts for message)
make upgrade # Apply pending migrations
make downgrade # Roll back the most recent migration
make db-status # Print current Alembic revision
make test # Run pytest with coverage for app/ and modules/
make lint # ruff + mypy + (optional) shellcheck on scripts/
make format # ruff format + ruff check --fix
make clean # Remove __pycache__, .pytest_cache, .mypy_cache, htmlcov/Bidirectional, incremental, rollback-safe sync between the local Docker DB and
prod. These targets live behind the Makefile so the prod URL never lands on
disk and the local DB credentials stay in .env.
# Sync both directions (newer updated_at wins, INSERT-only, no deletes)
make sync-db PROD_URL='postgres://user:pass@host:5432/db'
# Preview without writing anything (dry run)
make sync-db-dry PROD_URL='postgres://user:pass@host:5432/db'
# Show per-table sync watermarks on both sides
make sync-db-status PROD_URL='postgres://user:pass@host:5432/db'
# Reset watermarks to epoch (next run scans fully)
make sync-db-reset PROD_URL='postgres://user:pass@host:5432/db'Optional flags are forwarded via ARGS='...':
make sync-db PROD_URL='...' ARGS='--keep-dump --jobs 8 --verbose'
make sync-db PROD_URL='...' ARGS='--table expenses'Safety features baked into sync-db:
- Refuses to run unless both DBs are on the same Alembic revision
- Backs up BOTH DBs to
/tmpbefore any mutation; auto-restores on failure - Deletes are never propagated (INSERT-only); conflicts resolve to the latest
updated_at - Idempotent and resumable via the
sync_statewatermark table - Isolated test harness:
make sync-db-test-up/sync-db-test-sync/sync-db-test-drop(throwaway Postgres on port 5433)
POST /api/v1/auth/register → 200
{
"access_token": "<jwt>",
"refresh_token": "<jwt>",
"token_type": "bearer"
}GET /api/v1/users/me → 200
{
"id": "203a029c-f5a5-4c70-8115-219b807f7951",
"name": "Demo User",
"email": "demo@example.com",
"is_active": true,
"avatar_url": null,
"created_at": "2026-06-19T16:13:20.626774Z",
"updated_at": "2026-06-19T16:13:20.626785Z"
}POST /api/v1/trackers → 201
{
"id": "2d35de32-f10d-4aec-833e-0e94d5e9bdd6",
"name": "My Finances",
"currency": "USD",
"created_at": "2026-06-19T16:13:28.359432Z",
"updated_at": "2026-06-19T16:13:28.359438Z"
}GET /api/v1/trackers/{id}/categories → 200
[
{ "name": "Groceries", "color": "#22C55E" },
{ "name": "Transport", "color": "#3B82F6" },
{ "name": "Dining", "color": "#F97316" },
{ "name": "Subscriptions", "color": "#8B5CF6" },
{ "name": "Entertainment", "color": "#EC4899" },
{ "name": "Health", "color": "#14B8A6" },
{ "name": "Shopping", "color": "#EAB308" },
{ "name": "Utilities", "color": "#06B6D4" },
{ "name": "Coffee", "color": "#A855F7" },
{ "name": "Uncategorized", "color": "#78716C" }
]Base path: /api/v1
| Group | Endpoints |
|---|---|
| Auth | POST /auth/{register,login,refresh,sign-out} |
| Users | GET/PATCH /users/me, PATCH /users/me/password, POST/DELETE /users/me/avatar |
| Preferences | GET/PUT /preferences |
| Trackers | CRUD /trackers |
| Categories | CRUD /trackers/{id}/categories |
| Expenses | CRUD /trackers/{id}/expenses (filter/sort/paginate) |
| Budgets | CRUD /trackers/{id}/budgets, GET .../{id}/status |
| Budget Alerts | GET /trackers/{id}/budget-alerts |
| Dashboard | GET /trackers/{id}/dashboard |
| Reports | GET /trackers/{id}/reports/{summary,spending,category-breakdown,needs-vs-wants,year-comparison} |
backend/
├── app/
│ ├── main.py # FastAPI app entry point
│ ├── api/v1/ # Router registration
│ └── core/ # Config, DB session, security
├── modules/
│ ├── auth/ # JWT auth
│ ├── users/ # User profile
│ ├── trackers/ # Tracker workspaces
│ ├── categories/ # Expense categories
│ ├── expenses/ # Expense records
│ ├── budgets/ # Budget limits
│ ├── dashboard/ # Summary aggregations
│ └── reports/ # Detailed reports
├── alembic/ # Database migrations
├── tests/ # Pytest suite
├── scripts/ # Prod ↔ local bidirectional DB sync (sync-db.sh, sync_db.py)
├── docs/ # ARCHITECTURE.md, SCRIPTS_REVIEW.md, screenshots/
├── docker-compose.yml
├── Makefile
└── pyproject.toml
Copyright (c) 2026 Dipto Karmakar
This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
See the LICENSE file for full terms.
In plain English:
- You may study and use this code for personal/educational purposes
- If you modify and distribute it, you must open-source your version under AGPL-3.0
- You may not use this in a commercial product or SaaS without written permission from the author
- You must credit the original author (Dipto Karmakar) in any derivative work
For commercial licensing inquiries: diptokmk47@gmail.com


