A production-style digital wallet backend built with FastAPI, featuring double-entry accounting, idempotent transaction processing, async background workers, and Redis-backed middleware.
Live demo: https://wallet-tv64.onrender.com
- User authentication — registration, login, JWT bearer tokens (bcrypt + python-jose)
- Wallet management — create, retrieve, delete wallets (one wallet per user enforced)
- Transactions — deposit, withdraw, transfer with full validation
- Double-entry ledger — every transaction creates matching DEBIT/CREDIT
LedgerEntryrows for immutable audit trail - Idempotency — Redis-backed
X-Idempotency-Keymiddleware prevents duplicate processing on all mutating endpoints - Email receipts — async transactional emails sent via Dramatiq + Brevo (SendinBlue)
- Transaction history — paginated history with sender/receiver names
- Auto-migrations — Alembic for schema versioning; tables also auto-created on startup
| Layer | Technology |
|---|---|
| Framework | FastAPI (async) |
| ORM | SQLModel + SQLAlchemy 2.0 (async) |
| Database | SQLite (aiosqlite) / PostgreSQL-ready |
| Auth | JWT (python-jose) + bcrypt (passlib) |
| Queue | Dramatiq (Redis broker) |
| Cache / Locking | Redis (redis.asyncio) |
| Brevo (SendinBlue) API | |
| Migrations | Alembic |
├── main.py # FastAPI app entry point, lifespan, middleware
├── config.py # Environment variable configuration
├── workers.py # Dramatiq async workers (email receipts)
├── alembic/ # Database migrations
│ ├── env.py
│ └── versions/
├── core/
│ ├── security.py # Password hashing, JWT creation
│ └── limiter.py # Centralized rate limiter definitions (signup, auth,transactions)
|
├── db/
│ ├── models.py # SQLModel tables (User, Wallet, Transaction, LedgerEntry)
│ └── session.py # Async engine, session factory
├── dependencies/
│ └── auth.py # JWT bearer auth dependency
├── middleware/
│ └── idempotency.py # Redis-backed idempotency middleware
├── routers/
│ ├── auth.py # POST /signup, POST /token
│ ├── users.py # GET /users/me, GET /users/{id}
│ ├── wallet.py # POST/GET/DELETE /wallet
│ └── transaction.py # PATCH /deposit|withdraw|transfer, GET /history, GET /{id}/ledger
└── schemas/
├── user.py # UserCreate, UserResponse, Token
├── wallet.py # WalletCreate, WalletResponse
└── transaction.py # DepositWithdrawRequest, TransferRequest, LedgerEntryResponse
- Python 3.11+
- Redis (for idempotency + Dramatiq broker)
- A Brevo (SendinBlue) API key for email receipts (optional — worker failures are non-blocking)
# Clone and enter the project
cd wallet
# Create a virtual environment
python3 -m venv .venv
source .venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Copy and configure environment variables
cp .env.example .env
# Edit .env with your values (see Configuration)
# Run database migrations
alembic upgrade head
# Start the API server
uvicorn main:app --reloadNote: If you don't have a
requirements.txt, install manually:
pip install fastapi uvicorn sqlmodel sqlalchemy aiosqlite alembic python-dotenv python-jose passlib bcrypt redis dramatiq brevo httpx
In a separate terminal, start the Dramatiq worker to process email receipts:
dramatiq workersAll configuration is loaded from environment variables (.env file):
| Variable | Description | Default |
|---|---|---|
SECRET_KEY |
JWT signing secret | (required) |
ALGORITHM |
JWT algorithm | HS256 |
ACCESS_TOKEN_EXPIRE_MINUTES |
JWT token lifetime | 30 |
DATABASE_URL |
Database connection string | sqlite+aiosqlite:///database.db |
REDIS_URL |
Redis connection string | redis://localhost:6379 |
BREVO_API_KEY |
Brevo transactional email API key | (optional) |
SENDER_MAIL |
From address for email receipts | (optional) |
SENDER_NAME |
From name for email receipts | (optional) |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /signup |
No | Register a new user |
| POST | /token |
No | Login, receive JWT access token |
POST /signup
{ "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "password": "securepass123" }POST /token (form-encoded)
| Field | Value |
|---|---|
username |
User's email |
password |
User's password |
Response: { "access_token": "...", "token_type": "bearer" }
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /users/me |
Yes | Get the current authenticated user |
| GET | /users/{user_id} |
No | Get a user by ID |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /wallet |
Yes | Create a wallet (one per user) |
| GET | /wallet |
Yes | Get the authenticated user's wallet |
| DELETE | /wallet |
Yes | Delete the authenticated user's wallet |
POST /wallet
{ "balance": 1000, "currency": "USD" }All mutating transaction endpoints require the X-Idempotency-Key header (a UUID string) to ensure exactly-once processing.
| Method | Path | Auth | Idempotency | Description |
|---|---|---|---|---|
| PATCH | /transaction/deposit |
Yes | Required | Add funds to wallet |
| PATCH | /transaction/withdraw |
Yes | Required | Withdraw funds from wallet |
| PATCH | /transaction/transfer |
Yes | Required | Transfer funds to another wallet |
| GET | /transaction/history |
Yes | No | Paginated transaction list |
| GET | /transaction/{id}/ledger |
Yes | No | Double-entry ledger rows for a transaction |
PATCH /transaction/deposit
{ "amount": 500, "currency": "USD" }PATCH /transaction/withdraw
{ "amount": 200, "currency": "USD" }PATCH /transaction/transfer
{ "to_account_id": 2, "amount": 100, "currency": "USD" }GET /transaction/history — query parameters:
| Param | Default | Max |
|---|---|---|
limit |
10 | 100 |
offset |
0 | — |
Every financial transaction atomically creates two immutable LedgerEntry rows:
| Transaction | Debit Entry | Credit Entry |
|---|---|---|
| Deposit | System (null wallet) | Receiver wallet |
| Withdraw | Sender wallet | System (null wallet) |
| Transfer | Sender wallet | Receiver wallet |
Each ledger entry records:
- The affected
wallet_id(null = the system/external side) - The
entry_type(debit / credit) - The
amountandcurrency - A
balance_snapshot— the wallet's balance immediately after the entry
The unique constraint (transaction_id, entry_type) guarantees no duplicate legs per transaction.
The X-Idempotency-Key header enables safe retries:
- First request — processed normally, response cached in Redis for 24 hours
- Duplicate request within 24h — cached response returned immediately (
X-Cache-Lookup: HIT) - Concurrent duplicate — acquires a Redis lock; second request receives
409 Conflictuntil the first completes
Email receipts are dispatched asynchronously via Dramatiq:
# Start the worker (requires Redis running)
dramatiq workersThe mail_reciept actor generates HTML receipts for deposit, withdrawal, and transfer transactions and sends them via the Brevo API. Worker failures are independent of the API response — the transaction is committed before the task is enqueued.
- Wallet balance must never go negative (DB-level
CHECKconstraint) - Currency codes must be exactly 3 uppercase characters
- Passwords must be at least 8 characters
- Withdrawals and transfers require sufficient balance
- Transaction currency must match the wallet's currency
- Cannot transfer to your own wallet
- One wallet per user is enforced
# Generate a new migration after model changes
alembic revision --autogenerate -m "description of change"
# Apply pending migrations
alembic upgrade head
# Roll back one step
alembic downgrade -1Migrations are configured for async SQLite with batch mode enabled.
This project is currently a work in progress and does not include a license file.