Skip to content

Architecture Overview

The LMS is a three-tier application with an async request path, schema-per-tenant database isolation, and a Celery-powered task queue for background processing.

High-Level Architecture

┌──────────────┐     ┌──────────────────────────────────────────┐     ┌────────────┐
│              │     │  Django (ASGI via gunicorn + uvicorn)     │     │            │
│   React /    │────▶│                                          │────▶│ PostgreSQL │
│  react-admin │◀────│  django-ninja-extra async controllers    │◀────│   16+      │
│              │     │  django-pgschemas (schema routing)       │     │            │
└──────────────┘     │  django-allauth (session auth)           │     └────────────┘
                     │  django-guardian (object permissions)     │
┌──────────────┐     │  django-pghistory (audit triggers)       │     ┌────────────┐
│   Borrower   │────▶│                                          │────▶│   Redis     │
│   Portal     │◀────│                                          │◀────│            │
└──────────────┘     └────────────┬─────────────────────────────┘     └────────────┘
                                  │                                         │
                                  ▼                                         │
                     ┌──────────────────────────────────────────┐           │
                     │  Celery Workers                          │◀──────────┘
                     │  (interest accrual, delinquency,         │
                     │   dunning, portfolio snapshots, etc.)     │
                     └──────────────────────────────────────────┘

Components

  • Admin Dashboard: React 18+ with react-admin, providing the administrative interface for loan management operations
  • Borrower Portal: React 18+ with MUI and React Query, providing borrower self-service (payments, documents, support)
  • Backend: Django 5.x running on ASGI with fully async controllers, services, and selectors
  • Database: PostgreSQL 16+ with schema-per-tenant isolation via django-pgschemas
  • Task queue: Celery + Redis for async workflows, daily batch jobs, and Canvas orchestration
  • Audit: django-pghistory creates PostgreSQL triggers that automatically snapshot every tracked model change

Request Lifecycle

A typical API request flows through these layers:

  1. Domain routing --- DomainRoutingMiddleware resolves the tenant from the request domain and activates the correct PostgreSQL schema
  2. Middleware stack --- Security, session, CSRF, authentication, tenant status, locale activation
  3. Controller --- Async django-ninja-extra controller handles HTTP concerns (validation, permissions, throttling)
  4. Service --- Async business logic layer handles transactions and orchestration
  5. Selector --- Async read-only layer constructs complex querysets
  6. Model --- Django ORM interacts with the tenant's PostgreSQL schema

Middleware Stack

The middleware chain processes every request in order:

Order Middleware Purpose
1 DomainRoutingMiddleware Resolves tenant from domain, sets request.tenant
2 SecurityMiddleware Django security headers
3 SessionMiddleware Session handling
4 CsrfViewMiddleware CSRF protection
5 AuthenticationMiddleware User authentication
6 AccountMiddleware django-allauth session
7 TenantStatusMiddleware Checks tenant is active, returns 503 if suspended
8 TenantLocaleMiddleware Activates per-tenant locale, timezone, currency
9 MessageMiddleware Django messages framework

Authentication and Authorization

Authentication

  • django-allauth in headless mode provides session-based authentication
  • Sessions use httpOnly, Secure cookies
  • Auth endpoints at /api/v1/auth/ handle login, signup, password reset
  • GET /api/v1/users/me returns the current user with tenant role and RBAC permissions

Authorization (RBAC)

Five roles form an additive hierarchy:

Role Inherits Adds
viewer --- Read-only access to all resources
collector viewer Create/edit collection actions, create promise-to-pay
loan_officer collector CRUD borrowers/loans/collateral; create payments/fees/documents; loan lifecycle actions; servicing operations
admin loan_officer CRUD config resources (programs, products, templates); financial actions; ledger management
superadmin --- Wildcard access ({"action": "*", "resource": "*"})

Object-Level Permissions

django-guardian provides fine-grained object-level permissions:

  • Loan officers receive explicit permissions on their assigned loans and borrowers
  • Permissions are granted/revoked when assigned_officer_id changes
  • has_object_permission() filters querysets so officers only see their own assignments

Frontend RBAC

GET /api/v1/users/me returns permissions in react-admin's RBAC format:

{"action": "list", "resource": "borrowers"}
{"action": "*", "resource": "*"}
{"type": "deny", "action": "write", "resource": "borrowers.ssn_last_four"}

The admin frontend uses usePermissions() + hasPermission() to gate UI elements. The borrower portal uses IsBorrowerUser permission scoping all data to the authenticated borrower.

API Conventions

Async Controllers

All API endpoints use async class-based controllers via django-ninja-extra:

@api_controller("/borrowers", tags=["Borrowers"], permissions=[IsViewerOrAbove])
class BorrowerController(ControllerBase, ReactAdminControllerMixin):
    @http_get("", response=list[BorrowerOut])
    async def list_borrowers(self, sort: str = "", range: str = "", filter: str = ""):
        qs = await selectors.borrower_list(filters=filter or None, sort=sort or None)
        return await self.paginated_list(qs, "borrowers", range or None)

react-admin Compatibility

All CRUD endpoints conform to the react-admin REST data provider convention:

  • GET /{resource} --- list with sort, range, filter query params
  • GET /{resource}/{id} --- single resource with id field in response
  • List responses include Content-Range: items 0-24/100 header
  • Business actions use POST /{resource}/{id}/{action} (e.g., /loans/{id}/approve)
  • Nested resources use /{parent}/{id}/{child} (e.g., /borrowers/{id}/addresses)

OpenAPI Documentation

Every endpoint includes operation_id, summary, and a docstring description. Every schema field includes Field(description="..."). OpenAPI docs are served at /api/v1/docs.

Validation Errors

Pydantic validation errors are converted to react-admin's server-side validation format:

{
  "message": "Validation failed",
  "errors": {
    "field_name": "User-friendly error message"
  }
}

Model Conventions

Base Mixins

All tenant models inherit from mixins in common/models.py:

Mixin Fields
UUIDPrimaryKey id (UUID, auto UUIDv4)
Timestamped created_at, updated_at (auto-set)
ExternalID external_id (str, nullable, unique per tenant)
Archivable is_archived (bool), archived_at (datetime)

Field Conventions

Data Type Django Field
Money MoneyField (django-money) --- never DecimalField for money
Phone PhoneNumberField (django-phonenumber-field)
Rates/percentages DecimalField stored as decimal (0.065 = 6.5%)
Extensible data JSONField (JSONB)
Enums Django TextChoices (string-based)
Generic relations Django contenttypes (GenericForeignKey + GenericRelation)

Audit History

django-pghistory tracks changes via PostgreSQL triggers. Tracked models automatically get *Event tables that store full row snapshots on every insert and update. Over 15 models are tracked, including Borrower, Loan, Payment, Fee, LoanModification, and JournalEntry.

Internationalization

Backend i18n

  • Model fields, choices, and validators use gettext_lazy for deferred translation
  • Services and runtime error messages use gettext with % named substitution (never f-strings inside _())
  • Message catalogs extracted via makemessages, compiled via compilemessages
  • English and Spanish translations included

Per-Tenant Localization

  • Tenant.locale, Tenant.timezone, Tenant.default_currency fields
  • TenantLocaleMiddleware activates locale per request (priority: Accept-Language header, tenant locale, default)
  • Babel-based formatters for currency, dates, numbers, and percentages
  • Jinja2 filters registered on PDF and communication template environments

Frontend i18n

  • Admin: react-admin i18nProvider with ra-i18n-polyglot, English and Spanish
  • Portal: locale-aware formatting via browser Intl APIs, borrower language preference

See Also