Skip to content

About

A simple daily expense tracker

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Spendrift — Personal Finance Backend

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


What is Spendrift?

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.

Stack

  • 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

Features

Module Status
Auth Complete
Users Complete
Trackers Complete
Categories Complete
Expenses Complete
Budgets Complete
Dashboard Complete
Reports Complete

Getting Started

Prerequisites

  • Docker (for PostgreSQL)
  • Python 3.11+
  • uv package manager
  • S3-compatible object storage (Cloudflare R2, MinIO, AWS S3) for avatar uploads

Setup

# 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 run

API runs at http://localhost:8000
Interactive docs at http://localhost:8000/docs

Key Commands

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/

Database sync (prod <-> local Docker)

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 /tmp before 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_state watermark table
  • Isolated test harness: make sync-db-test-up / sync-db-test-sync / sync-db-test-drop (throwaway Postgres on port 5433)

Screenshots

API Documentation (Swagger UI)

Swagger UI Overview

Auth Endpoints

Auth Endpoints

Tracker Endpoints

Tracker Endpoints

Expense Endpoints

Expense Endpoints

Sample API Responses

Register / Login

POST /api/v1/auth/register → 200
{
  "access_token": "<jwt>",
  "refresh_token": "<jwt>",
  "token_type": "bearer"
}

Get current user

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"
}

Create a tracker (auto-seeds 10 categories)

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"
}

Auto-seeded categories

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" }
]

API Overview

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}

Project Structure

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

License

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

About

A simple daily expense tracker

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages