InvoicePDFs — Architecture at a Glance
Layered Architecture
┌─────────────────────────────────────────────────────────┐
│ main.py (composition root) │
│ FastAPI factory, router wiring, dependency injection │
├─────────────────────────────────────────────────────────┤
│ │
│ HORIZONTAL LAYERS (cross-cutting, used by all features) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌─────────────┐ │
│ │ Auth / │ │ Pagina- │ │ Audit │ │ Rate │ │
│ │ Deps │ │ tion │ │ │ │ Limiter │ │
│ └──────────┘ └──────────┘ └─────────┘ └─────────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌─────────────┐ │
│ │ Errors │ │ Config │ │ Storage │ │ Schemas │ │
│ └──────────┘ └──────────┘ └─────────┘ └─────────────┘ │
│ │
├─────────────────────────────────────────────────────────┤
│ │
│ VERTICAL FEATURE SLICES (api/v1/*.py) │
│ Each slice owns its route handlers and business logic │
│ │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ Invoice │ │ Customer │ │ Batch │ │Credit Notes │ │
│ ├─────────┤ ├──────────┤ ├──────────┤ ├─────────────┤ │
│ │Template │ │ Payment │ │ Webhook │ │ Files │ │
│ ├─────────┤ ├──────────┤ ├──────────┤ ├─────────────┤ │
│ │Branding │ │Tax Rates │ │ Jobs │ │ Imports │ │
│ ├─────────┤ ├──────────┤ ├──────────┤ ├─────────────┤ │
│ │Numbering│ │Workspace │ │ Audit │ │ Renders │ │
│ └─────────┘ └──────────┘ └──────────┘ └─────────────┘ │
│ │
├─────────────────────────────────────────────────────────┤
│ │
│ ENGINE (pure compute, zero app.* imports) │
│ Can be extracted as a standalone library │
│ │
│ ┌────────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │ Validation │ │ Calculation │ │ Renderer (WeasyPrint)│ │
│ └────────────┘ └─────────────┘ └──────────────────────┘ │
│ ┌────────────────┐ ┌──────────────┐ │
│ │Template Engine │ │ Render Model │ │
│ └────────────────┘ └──────────────┘ │
│ │
├─────────────────────────────────────────────────────────┤
│ │
│ DATABASE (SQLAlchemy 2.0) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Models │ │ Session │ │ Init DB │ │
│ │ (30 tbl) │ │ Factory │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Horizontal Layers
These are cross-cutting concerns shared by all (or most) vertical feature slices.
| Layer |
Files |
Responsibility |
| Auth & Identity |
api/deps.py, auth/api_keys.py |
API key extraction/validation, AuthContext (account + key), monthly quota enforcement, idempotency key handling |
| Pagination |
api/pagination.py |
Cursor-based pagination: encode_cursor, decode_cursor, apply_cursor_filter, finalize_cursor_page |
| Audit |
audit.py |
emit_audit_event() — captures actor, action, resource, IP, user-agent, request_id. Called before db.commit() in all write operations |
| Rate Limiting |
middleware/rate_limit.py |
Sliding-window in-memory rate limiter. Configurable via RATE_LIMIT_PER_SECOND and RATE_LIMIT_BURST |
| Error Handling |
errors.py |
raise_api_error() for consistent {error: {code, message}} envelope. Global exception handlers |
| Schemas |
schemas/v1.py |
All Pydantic v2 request/response models (~120 classes). Single source of truth for API contracts |
| Configuration |
config.py |
Pydantic Settings loaded from environment variables |
| Storage |
storage/ |
Abstract Storage ABC + LocalFilesystemStorage (default) + GCSStorage (Google Cloud Storage). Backend selected via STORAGE_BACKEND env var |
| App Composition |
main.py |
create_app() factory: wires routers, middleware, error handlers, state (engine, storage, sessions) |
| Database |
db/models.py, db/session.py, db/base.py, db/init_db.py |
30 SQLAlchemy models, session factory, schema initialization |
Storage
The storage layer abstracts file persistence behind a common interface, allowing the application to run on local disk in development and cloud storage in production.
Architecture (app/storage/)
| File |
Role |
base.py |
Storage ABC with 5 methods: save_pdf, load_pdf, save_file, load_file, delete_file |
local.py |
LocalFilesystemStorage — writes to STORAGE_LOCAL_DIR (default .data/). Default backend |
gcs.py |
GCSStorage — Google Cloud Storage backend using google-cloud-storage SDK |
__init__.py |
create_storage(settings) factory — selects backend from config, lazy-imports GCS only when needed |
Configuration
| Env Var |
Default |
Description |
STORAGE_BACKEND |
local |
local or gcs |
STORAGE_LOCAL_DIR |
.data |
Directory for local storage |
GCS_BUCKET |
(required for gcs) |
Google Cloud Storage bucket name |
GCS_PREFIX |
(empty) |
Optional key prefix within the bucket |
Usage
# Local development (default)
STORAGE_BACKEND=local
# Google Cloud Storage
STORAGE_BACKEND=gcs
GCS_BUCKET=my-invoicepdfs-bucket
GCS_PREFIX=prod
# Install GCS dependencies
uv pip install invoicepdfs[gcs]
Vertical Feature Slices
Each slice is a self-contained feature with its own route file in api/v1/. Slices are independent of each other — they only depend on horizontal layers.
| Feature |
Route File |
Key Models |
Horizontal Deps |
| Invoices |
invoices.py (1,256 lines) |
Invoice, InvoiceRender |
auth, pagination, audit, engine, storage, webhooks |
| Customers |
customers.py |
Customer |
auth, pagination, audit, idempotency |
| Business Profiles |
business_profiles.py |
BusinessProfile |
auth, pagination, audit |
| Templates |
templates.py |
(engine-based) |
auth, engine, storage |
| Template Versions |
template_versions.py |
TemplateVersion |
auth, audit |
| Documents |
documents.py |
Render, UsageEvent |
auth, engine, storage |
| Renders |
renders.py |
Render |
auth, storage |
| Batches |
batches.py |
Batch, BatchItem, Job |
auth, pagination, engine, storage |
| Credit Notes |
credit_notes.py |
CreditNote |
auth, pagination, audit, engine |
| Payments |
payments.py |
Payment |
auth, pagination, audit |
| Deliveries |
deliveries.py |
Delivery |
auth |
| Files |
files.py |
FileAsset |
auth, audit, storage |
| Branding |
branding.py |
BrandingSettings |
auth, audit, storage |
| Attachments |
attachments.py |
InvoiceAttachment |
auth |
| Numbering |
numbering.py |
NumberingSequence |
auth, pagination, audit |
| Tax Rates |
tax_rates.py |
TaxRate |
auth, pagination, audit |
| Webhooks |
webhooks.py |
WebhookEndpoint, WebhookEvent |
auth, pagination, audit |
| Jobs |
jobs.py |
Job |
auth, audit |
| Imports |
imports.py |
ImportRecord, Job |
auth, audit |
| Workspaces |
workspaces.py |
Workspace, WorkspaceMember |
auth, pagination |
| API Keys |
api_keys.py |
ApiKey |
auth, audit |
| Usage |
usage.py |
UsageEvent |
auth |
| Audit Log |
audit.py (route) |
AuditEvent |
auth, pagination |
| Reference Data |
reference.py |
(none) |
(none) |
Engine (Pure Compute Layer)
The engine has zero app.* imports — it operates on pure data structures and could be extracted as a standalone Python library.
InvoiceData (input)
│
├── validate() → ValidationError or pass
├── calculate() → CalculationResult (subtotal, taxes, discounts, shipping, total)
└── render() → EngineResult (pdf_bytes, calculation, render_id, timestamps)
│
├── template_engine → Jinja2 HTML
├── render_model → template context dict
└── renderer → WeasyPrint HTML→PDF
| File |
Role |
engine/schemas.py |
Canonical data model: InvoiceData, LineItem, Party, Address, etc. |
engine/validation.py |
Business rules: non-empty line items, positive quantities, valid discount values |
engine/calculation.py |
Tax computation (inclusive/exclusive), discounts (line/document), shipping, totals |
engine/template_engine.py |
Jinja2 template discovery, render_invoice_html() |
engine/render_model.py |
Builds template context with formatted currency, dates, line amounts |
engine/renderer.py |
WeasyPrintRenderer — HTML to PDF conversion |
engine/engine.py |
InvoiceEngine orchestrator: validate → calculate → render |
Request Flow
Every API request follows the same pattern:
Request
→ Rate Limiter (middleware)
→ Auth (extract API key → load Account)
→ Route Handler
→ Validate input (Pydantic schema)
→ [Idempotency check]
→ Business logic (DB queries, engine calls)
→ emit_audit_event()
→ db.commit()
→ [fire_event() for webhooks]
→ Response ({data: ...} envelope)
Cross-Feature Integration Points
- Tax Rates → Invoices:
_resolve_tax() snapshots tax rate values at invoice creation time
- Jobs → Batches:
create_batch() creates a Job; batch processor updates job.progress_current per-item
- Jobs → Imports:
confirm_import() creates a completed Job record
- Audit → All writes: Every mutation emits an audit event in the same DB transaction
- Webhooks → Batch/Invoice events:
fire_event() dispatches asynchronously via thread pool
- Attachments → Invoices + Files: Validates ownership of both before linking
Codebase Stats
| Metric |
Value |
Python files in app/ |
60 |
| Total lines (app/) |
~10,100 |
| SQLAlchemy models |
30 |
| Pydantic schemas |
~120 classes |
| API endpoints |
~85 across 26 route files |
| Largest files |
invoices.py (1,256), schemas/v1.py (1,199), models.py (555) |