Portfolio

Case study

Rendered from the project README. The demo uses fictional data.

LedgerLite

Portfolio demo of an invoicing REST API. The company is Vandelay Industries. The company, the customers, and every amount are fictional. This is a demo. Sandbox tenants are deleted after 60 minutes.

Overview

LedgerLite is a small FastAPI service for accounts receivable: customers, invoices, payments, an event stream that is stored and never sent anywhere, and an aging report. Authentication is a password grant plus a hashed API key. Writes require a scope. The public reader login and the public read-only API key cannot write.

The live process listens on 127.0.0.1:18809 behind the portfolio nginx prefix /portfolio/ledgerlite/. SQLite is the database on that host. DATABASE_URL=postgresql+asyncpg://... is the switch for Postgres; the VPS image does not install that driver.

Problem → Solution → Result

Problem

Solution

Result

These are capabilities of this demo, not claims about a client engagement.

Features

Architecture

Browser
         |
         |  relative URLs on the landing page
         v
      edge nginx, prefix /portfolio/ledgerlite/ stripped
         |
         v
      systemd: python -m app   127.0.0.1:18809
         |
         +--> SQLite   DATA_DIR/ledgerlite.db
         +--> signing key  DATA_DIR/jwt.key   (mode 0600, created on first use)

docker-compose.yml is the shape for a local API plus an optional postgres:16 profile. The public demo does not run Compose.

API

MethodPathScope
POST/v1/auth/tokenpublic password grant
POST/v1/auth/refreshpublic, rotates the refresh token
GET/v1/meany authenticated caller
GET/POST/v1/customerscustomers:read / customers:write
GET/PATCH/DELETE/v1/customers/{id}read / write; PATCH needs If-Match
GET/v1/invoices and /v1/invoices.csvinvoices:read
POST/v1/invoicesinvoices:write and Idempotency-Key
POST/v1/invoices/{id}/send and /voidinvoices:write
POST/v1/invoices/{id}/paymentspayments:write and Idempotency-Key
GET/v1/paymentsinvoices:read
GET/v1/eventsinvoices:read
GET/v1/reports/agingreports:read

Errors use RFC 9457 application/problem+json. Swagger UI is /docs. ReDoc is /redoc. Both load /openapi.json and the files under /static/vendor/ with the ROOT_PATH prefix when it is set. The password flow token URL is the relative path v1/auth/token.

Tests

cd ledgerlite
      /home/box/.local/bin/python3.9 -m venv .venv
      .venv/bin/pip install -r requirements.txt -r requirements-dev.txt
      .venv/bin/python -m pytest -q --cov=app
      .venv/bin/ruff check app tests
      .venv/bin/python scripts/record_tests.py

Python 3.9 is the version the VPS runs. Annotations use Optional, List, and Dict so Pydantic and FastAPI can evaluate them on 3.9.

Deployment notes

Screenshots

Desktop landing page

Mobile landing page

Swagger UI

Open the live demo