A self-hosted personal finance tracker with automatic bank sync via Plaid, rule-based transaction categorization, recurring transaction detection, and an encrypted credential store.
- Automatic Bank Sync — Connect bank accounts via Plaid and sync transactions on a configurable schedule (default: every 6 hours)
- Rule-Based Categorization — Classify transactions automatically using merchant name regex, MCC codes, and amount ranges
- Recurring Detection — Identifies recurring charges (subscriptions, bills) using both Plaid's recurring streams and custom pattern matching
- Encrypted Token Storage — Plaid access tokens are AES-encrypted at rest using a key you control
- Dashboard — Jinja2-templated web UI with account overview, transaction search, reports, and rule management
- Single-User Auth — bcrypt-hashed password with session-based login and brute-force lockout (Redis-backed)
- Rate Limiting — Per-IP in-process rate limiter on sensitive endpoints
- Health Checks —
/health(lightweight) and/readyz(DB + Redis verification)
| Component | Technology |
|---|---|
| Backend | Python 3.12+, FastAPI, SQLAlchemy |
| Database | MySQL 8.0+ (MariaDB compatible) |
| Cache/Sessions | Redis 5.0+ |
| Bank Data | Plaid API |
| Scheduling | APScheduler |
| Encryption | cryptography (Fernet) |
| Auth | bcrypt + Redis session store |
| Frontend | Jinja2 templates (server-rendered) |
- Python 3.12+
- MySQL 8.0+ or MariaDB 10.6+
- Redis 5.0+
- A Plaid account (sandbox is free)
cd server
# Create virtual environment
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# Install dependencies
pip install -r requirements.txt
# Configure environment
cp .env.template .env
# Edit .env with your database credentials, Plaid keys, and encryption keyRun the migrations in order:
mysql -u ledger_api -p ledger < migrations/001_initial_schema.sql
mysql -u ledger_api -p ledger < migrations/002_starter_rules.sql
mysql -u ledger_api -p ledger < migrations/003_simplify_business_units.sql
mysql -u ledger_api -p ledger < migrations/004_fix_mcc_column_length.sql# Generate encryption key for Plaid token storage
python -c "import os; print(os.urandom(32).hex())"
# Generate admin password hash
python -c "import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())"uvicorn app.main:app --host 127.0.0.1 --port 8084Visit http://localhost:8084 to access the dashboard.
All configuration is via environment variables (see .env.template for the full list):
| Variable | Default | Description |
|---|---|---|
APP_ENV |
development |
Environment mode |
DB_HOST |
localhost |
MySQL host |
DB_NAME |
ledger |
Database name |
PLAID_CLIENT_ID |
— | Your Plaid client ID |
PLAID_SECRET |
— | Your Plaid secret key |
PLAID_ENV |
production |
sandbox or production |
PLAID_ENCRYPTION_KEY |
— | Hex-encoded 32-byte key for token encryption |
SYNC_INTERVAL_HOURS |
6 |
How often to sync transactions |
ADMIN_USERNAME |
admin |
Dashboard login username |
ADMIN_PASSWORD_HASH |
— | bcrypt hash of admin password |
REDIS_DB |
5 |
Redis database number |
DISPLAY_TIMEZONE |
US/Eastern |
Timezone for dashboard display |
server/
├── app/
│ ├── main.py # FastAPI app, middleware, lifespan
│ ├── config.py # Pydantic settings (env-backed)
│ ├── database.py # SQLAlchemy engine + session factory
│ ├── encryption.py # AES encryption for Plaid tokens
│ ├── models.py # SQLAlchemy ORM models
│ ├── plaid_client.py # Plaid API client singleton
│ ├── recurring.py # Recurring transaction detection
│ ├── rules.py # Rule engine for categorization
│ ├── scheduler.py # APScheduler background jobs
│ ├── sync.py # Plaid transaction sync logic
│ └── dashboard/
│ ├── api.py # REST API endpoints
│ ├── auth.py # Login, sessions, lockout
│ ├── routes.py # Template-rendered pages
│ └── templates/ # Jinja2 HTML templates
├── deploy/
│ ├── ledger-api.service # systemd unit (example)
│ └── nginx-ledger-api.conf # nginx reverse proxy (example)
└── migrations/
├── 001_initial_schema.sql
├── 002_starter_rules.sql
├── 003_simplify_business_units.sql
└── 004_fix_mcc_column_length.sql
Example systemd and nginx configs are provided in server/deploy/. These are templates — update the domain, paths, and user to match your environment.
- Sign up at dashboard.plaid.com
- Use sandbox mode for testing (free, uses test credentials)
- For production, apply for Plaid access (approval required for production credentials)
- Add your
PLAID_CLIENT_IDandPLAID_SECRETto.env - Generate an encryption key and add as
PLAID_ENCRYPTION_KEY - Use the dashboard to link accounts via Plaid Link
- Plaid access tokens are encrypted at rest (AES via Fernet)
- Admin password stored as bcrypt hash (never plaintext)
- Redis-backed login lockout after failed attempts
- Per-IP rate limiting on authentication and API endpoints
- No secrets in source code — everything via
.env - systemd hardening:
NoNewPrivileges,ProtectSystem=strict,PrivateTmp
MIT — see LICENSE.
Built by Wigley Studios