Documentation
Everything needed to install, understand, operate and extend the Danube Bank banking system.
Overview
Danube Bank is a two-application system sharing one MySQL database:
- Customer app (
app/, port 8000) — online banking for customers. - Operator app (
admin_app/, port 8001, “Danube Ops”) — bank management, including live database tools.
Both import the same ORM models and the same banking service layer, so there is exactly one implementation of money movement. The operator app uses the same ledger functions rather than editing balances directly.
| Layer | Technology |
|---|---|
| Web framework | FastAPI + Starlette |
| ORM | SQLAlchemy 2.0 (typed mappings) |
| Database | MySQL 8.4 in Docker (utf8mb4) |
| Templates | Jinja2, server-rendered |
| Sessions | Signed cookies (`SessionMiddleware`) |
| Hashing / crypto | scrypt (stdlib), Fernet (cryptography) |
| Tests | pytest, 14 unit + ledger integration tests |
Quick start
Requirements: Docker Desktop and Python 3.11+.
docker compose up -d
python -m venv .venv
.venv\Scripts\activate # Windows
pip install -r requirements.txt
cp .env.example .env
python -m scripts.seed
uvicorn app.main:app --reload
uvicorn admin_app.main:app --port 8001
Open http://127.0.0.1:8000 for customers and http://127.0.0.1:8001 for operators.
| Password | Access | |
|---|---|---|
alex@danube.demo | Password123! | Customer with accounts, card and loan |
maria@danube.demo | Password123! | Customer, KYC pending |
andrei@danube.demo | Password123! | Customer, useful transfer target |
admin@danube.demo | Password123! | Operator / admin |
Reset the database at any time with python -m scripts.seed --reset.
Configuration
Environment (.env)
| Variable | Purpose |
|---|---|
SECRET_KEY | Signs session cookies and derives the card-encryption key |
DATABASE_URL | SQLAlchemy URL, default mysql+pymysql://banker:bankerpass@127.0.0.1:3306/banking |
DEMO_PASSWORD | Password assigned to seeded demo users |
ALLOW_ADMIN_SQL_WRITE | 1 enables INSERT/UPDATE/DELETE in the operator SQL console |
Runtime settings (MySQL)
Institution name, support email, currency, transfer and cash limits, overdraft, savings interest,
card limits, loan rates and limits, maintenance mode and demo-login visibility all live in the
settings table. Operators edit them in Settings; both apps refresh their
cache every 10 seconds. Changes are audited individually.
Architecture
app/
main.py customer app entrypoint and maintenance gate
models.py customers, accounts, cards, ledger, loans, settings, audit
security.py scrypt hashing, Luhn, Fernet-encrypted PANs
deps.py auth, CSRF, template rendering
routers/ auth, accounts, transfers, cards, loans, admin (customer-side)
services/banking.py the only code that moves money
services/settings.py cached DB settings
services/queries.py read models for dashboards and statements
templates/ static/
admin_app/
main.py operator app entrypoint (port 8001)
auth.py operator login (role=admin)
views.py dashboard, customers, accounts
banking_ops.py transactions, reversals, cards, loans
system.py settings, DB explorer, SQL console, backups, audit
templates/ static/
scripts/seed.py schema, settings defaults, demo data
docs/ this GitHub Pages site
tests/ unit and ledger integration tests
Ledger design
Money is stored in integer cents. A transaction is a header row; each account touched gets a
ledger_entries row with the signed amount and the resulting balance.
Transaction TXN-8F2A… transfer 250.00 USD
├─ LedgerEntry account A −25000 balance_after 175000
└─ LedgerEntry account B +25000 balance_after 25000
post_transaction()locks every involved account withSELECT … FOR UPDATE, ordered by id to avoid deadlocks.- It validates account status, currency, and available funds (balance + overdraft) before writing.
- Multi-entry transactions must sum to zero. Single-sided entries represent the bank's external position for deposits, withdrawals, interest and card payments.
- Idempotency keys are supported and unique, so retried requests cannot double-charge.
- Reversal posts the mirror entries and marks the original
reversed; nothing is deleted.
Security model
- Passwords and card PINs use scrypt with per-secret salts; comparisons are constant-time.
- Card PANs are Fernet-encrypted; lists only ever show the last four digits. Reveal is a separate audited request and can be disabled in Settings.
- Every mutating form carries a CSRF token bound to the session.
- Customers can only read or move money on accounts they own; ownership is checked server-side on every route.
- The operator app has its own cookie name and signing key, and refuses non-admin accounts.
- All operator actions are appended to
audit_logswith actor, IP and detail.
Accounts, transfers and statements
Customers can hold up to the configured number of open accounts. Checking accounts carry an overdraft facility; savings accounts accrue interest through the daily batch job.
Transfers are local (same bank, same currency). The recipient account is resolved by account number, validated, and both accounts post in one database transaction. Receipts are addressable by reference, CSV exports and printable statements are generated from ledger entries.
Standing orders support daily, weekly and monthly frequencies. When the batch job runs, due plans execute; if funds are insufficient the plan is paused and the customer is notified.
Cards
Cards use the demo BIN 400000 and a Luhn check digit. Issuing generates a 4-digit PIN
unless one is supplied. Daily limits are enforced per card and reset automatically on the next day.
Operators can freeze, cancel, change limits and reset PINs from either app.
Loans
Loan pricing is a base rate with discounts for verified KYC and larger amounts, all configurable in Settings. Approval creates an annuity amortisation schedule, disburses principal to the chosen account and marks instalments paid as they are repaid. Early repayment closes the loan immediately.
Operator app (Danube Ops)
Sign in at http://127.0.0.1:8001 with an account whose role is admin.
- Customers — create, edit, lock/unlock, change role, reset password, set KYC.
- Accounts — open accounts, freeze/activate/close, post audited credit or debit adjustments.
- Transactions — search the ledger, inspect entries per account, reverse a completed transaction with a reason.
- Cards — issue, reveal, freeze, cancel, set limits, reset PIN.
- Loans — approve, reject, or book and disburse a loan on behalf of a customer.
- Settings — edit every runtime business rule.
- Audit — search every operator and system event.
Database tools
The operator app includes a real database explorer:
- Browse any table with sorting and pagination.
- Export any table to CSV.
- Backup streams a full SQL dump (
CREATE TABLE+INSERT) with foreign-key checks disabled. - SQL console — SELECT always allowed; INSERT/UPDATE/DELETE only when
ALLOW_ADMIN_SQL_WRITE=1; DROP/TRUNCATE/ALTER/CREATE/GRANT always blocked. Every statement is audited.
HTTP endpoints
The customer app is server-rendered, so most routes return HTML. This table lists the route groups.
| Customer app (:8000) | Purpose |
|---|---|
GET /health | JSON health check |
GET/POST /login, /register | Authentication |
GET /dashboard | Balances, chart, recent activity |
GET /accounts/{id}, /statement, /export.csv | Account detail and exports |
POST /accounts/{id}/deposit|withdraw | Cash movements |
GET/POST /transfer, GET /receipt/{ref} | Transfers and receipts |
GET/POST /beneficiaries, /scheduled | Payees and standing orders |
GET/POST /cards, POST /cards/{id}/… | Card lifecycle |
GET/POST /loans, POST /loans/{id}/… | Loans and repayment |
GET /admin/… | Basic admin pages for role=admin |
GET /api/docs | OpenAPI documentation (FastAPI) |
| Operator app (:8001) | Purpose |
|---|---|
GET /health | JSON health check |
GET /customers, /customers/{id} | Customer management |
GET /accounts, /accounts/{id} | Account management and adjustments |
GET /transactions, /transactions/{ref} | Ledger inspection and reversal |
GET /cards, /loans | Card and loan operations |
GET/POST /settings | Runtime configuration |
GET /database, /database/{table}, /database/backup.sql | Database explorer and backup |
GET/POST /sql | Audited SQL console |
GET /audit | Audit trail |
Testing
pytest
Unit tests cover Luhn generation, scrypt hashing, annuity maths and money formatting. Ledger tests run against MySQL, verify balanced entries, overdraft limits, ownership checks and idempotency, then roll back so they leave no data behind. If MySQL is not reachable they skip.
Deployment
For a shared environment:
- Set a strong
SECRET_KEYand change every database password. - Run behind a TLS-terminating reverse proxy (Caddy, nginx, Cloudflare Tunnel).
- Set
https_only=Trueon both session middlewares. - Disable the demo login shortcuts and card reveal in Settings.
- Schedule
POST /admin/run-jobs(customer app) or use the operator app to run daily accruals. - Keep MySQL on a private network and back it up using the operator backup endpoint or
mysqldump.
This documentation site is published with GitHub Pages from docs/ and served at
bankingsystem.danubelabs.net.
Troubleshooting
| Symptom | Fix |
|---|---|
Can't connect to MySQL | docker compose ps — wait for the healthcheck, then rerun. |
| Login says password is wrong | Reseed: python -m scripts.seed --reset. |
404 on /accounts/new | Ensure dynamic /accounts/{id} routes are declared after static ones. |
| Settings changes not visible | Wait 10 seconds for the cache refresh, then reload. |
| Card reveal disabled | Enable it under Settings → Security. |
Questions or ideas?
Open an issue on GitHub — the project is meant to be read and extended.