A backend system for managing financial records with role-based access control. Built with FastAPI and PostgreSQL.
I built this as a screening assignment for Zorvyn. The goal was to create a clean, well-structured backend that handles real-world concerns like authentication, role-based permissions, data validation, and dashboard-level aggregations.
Different people in an organisation need different levels of access to financial data. A junior employee shouldn't be able to delete records. A manager should be able to see summaries but not necessarily manage users. This backend handles all of that cleanly.
There are three roles:
- Viewer — can browse records and see the dashboard. Nothing else.
- Analyst — can create and update records on top of everything a viewer can do.
- Admin — full access. Can manage users, change roles, and delete records.
- FastAPI — fast, modern Python web framework with automatic docs
- PostgreSQL — relational database
- SQLAlchemy (async) — talks to the database using Python instead of raw SQL
- JWT via python-jose — stateless authentication tokens
- Passlib + bcrypt — passwords are hashed, never stored as plain text
- Pydantic v2 — validates every request automatically
git clone <your-repo-url>
cd finance-backendpython3 -m venv venv
source venv/bin/activatepip install -r requirements.txtpsql postgresThen run these inside psql:
CREATE DATABASE finance_db;
CREATE USER finance_user WITH PASSWORD 'finance123';
GRANT ALL PRIVILEGES ON DATABASE finance_db TO finance_user;
\c finance_db
GRANT ALL ON SCHEMA public TO finance_user;
\qDATABASE_URL=postgresql+asyncpg://finance_user:finance123@localhost:5432/finance_db
SECRET_KEY=supersecretkey123changethisinproduction
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
uvicorn app.main:app --reloadhttp://127.0.0.1:8000/docs
FastAPI generates this automatically. You can test every endpoint directly from the browser — no Postman needed.
- Register a user at
POST /auth/register - Login at
POST /auth/login— you get back a JWT token - Click Authorize in the docs page and paste the token
- All future requests will automatically include it
Tokens expire after 30 minutes. Roles are checked on every protected endpoint using FastAPI's dependency injection system.
| Action | Viewer | Analyst | Admin |
|---|---|---|---|
| View records | Yes | Yes | Yes |
| Create records | No | Yes | Yes |
| Update records | No | Yes | Yes |
| Delete records | No | No | Yes |
| View dashboard | Yes | Yes | Yes |
| Manage users | No | No | Yes |
POST /auth/register— create a new userPOST /auth/login— login and receive a JWT token
GET /users/me— see your own profileGET /users/— list all users (admin only)PATCH /users/{id}— update role or deactivate a user (admin only)
POST /records/— create a new record (analyst and admin)GET /records/— list all records with optional filtersGET /records/{id}— fetch one specific recordPATCH /records/{id}— update a record (analyst and admin)DELETE /records/{id}— soft delete a record (admin only)
GET /dashboard/summary— total income, total expenses, net balanceGET /dashboard/categories— breakdown by categoryGET /dashboard/recent— last 5 transactionsGET /dashboard/trends— month by month income and expense totals
The records list endpoint supports query parameters so the frontend can filter without loading everything:
GET /records/?type=income
GET /records/?category=Salary
GET /records/?start_date=2026-01-01&end_date=2026-04-30
GET /records/?skip=0&limit=10
Soft deletes — records are never permanently removed. When something is deleted, we just set is_deleted = True and hide it from all responses. This means data can be recovered if needed and nothing is lost by accident.
Database-level aggregations — the dashboard doesn't load all records into Python and loop through them. It uses SQL GROUP BY and SUM directly, which is much faster as data grows.
Role guards as reusable dependencies — access control is handled through FastAPI's Depends() system. A single function like require_admin can be dropped into any endpoint. No repeated if/else checks everywhere.
Passwords never leave the system — the response schemas are designed so that even the hashed password is never included in any API response.
- New users are viewers by default — an admin must manually upgrade them
- The
.envfile is never committed to version control - All dashboard endpoints require login but no specific role
- Pagination defaults to 10 records per page, max 100
finance-backend/
├── app/
│ ├── main.py ← entry point, table creation on startup
│ ├── core/
│ │ ├── config.py ← reads .env settings
│ │ ├── security.py ← password hashing and JWT logic
│ │ └── deps.py ← reusable auth and role guards
│ ├── models/ ← database table definitions
│ ├── schemas/ ← request and response shapes
│ ├── routers/ ← API endpoints
│ ├── services/ ← business logic
│ └── db/ ← database connection and base class
├── .env
├── requirements.txt
└── README.md