The backend follows a modular-monolith structure. Domain boundaries are explicit, while deployment and transactions remain simple enough for an in-house team. A module can be extracted into a service later only when scale, ownership, or isolation requires it.
backend/
├── alembic.ini
├── alembic/
│ ├── env.py # Async Alembic environment
│ ├── script.py.mako # Revision template
│ └── versions/ # Ordered schema revisions
├── app/
│ ├── api/
│ │ └── v1/
│ │ ├── applications.py # Application HTTP endpoints
│ │ ├── ai.py # Optional advisory AI endpoints
│ │ ├── audit.py # Audit-event endpoints
│ │ ├── documents.py # Private storage ticket endpoints
│ │ ├── migration_manifests.py # Safe mapping validation endpoint
│ │ ├── password_recovery.py # Local reset and SSO recovery endpoints
│ │ ├── portfolio.py
│ │ ├── user_management.py # Directory, profile, and invitation endpoints
│ │ └── router.py # v1 router composition
│ ├── core/
│ │ └── config.py # Environment configuration
│ ├── db/
│ │ ├── base.py # Declarative metadata
│ │ └── session.py # Async engine and transaction scope
│ ├── integrations/
│ │ ├── ai/ # Governed model-provider adapters
│ │ ├── email/ # Server-only SMTP delivery adapter
│ │ ├── sms/ # Server-only generic HTTP SMS adapter
│ │ └── storage/ # Private S3 document adapter
│ ├── legacy_migration/
│ │ ├── cli.py # Manifest validation/planning CLI
│ │ └── manifest.py # Mapping and safety contracts
│ ├── models/ # SQLAlchemy persistence models
│ ├── repositories/
│ │ └── users.py # Shared persistence contract, no HTTP imports
│ ├── schemas/
│ │ ├── password_recovery.py # Reset request/response contracts
│ │ └── user_management.py # User profile and lifecycle contracts
│ ├── services/
│ │ ├── password_recovery.py # Token, email, hash, and SSO routing use cases
│ │ ├── recovery_notifications.py # Independent email/SMS channel orchestration
│ │ └── user_management.py # Directory and profile lifecycle use cases
│ ├── fixtures.py # Prototype-only records
│ ├── user_fixtures.py # Prototype-only user/auth records
│ └── main.py # FastAPI application factory
├── tests/
├── requirements.txt
└── requirements-dev.txt
backend/app/
├── api/ # Transport only: validation, auth dependency, response mapping
├── core/ # Config, security, observability, errors, idempotency
├── db/ # Engine, sessions, metadata, transaction helpers
├── models/ # Persistence models only
├── schemas/ # External and internal data contracts
├── repositories/ # Database queries; no HTTP/provider code
├── services/ # Use cases and transaction boundaries
├── workflows/ # Validated loan state machine and orchestration
├── integrations/ # CKYC, PAN, bureau, AA, e-sign, OCR adapters
├── legacy_migration/ # Read-only extract/transform/reconcile pipeline
├── tasks/ # Async jobs and scheduled reconciliation
└── api/v2/ # Only when a breaking API contract is required
api -> services -> repositories -> models/db
-> integrations
-> workflows
legacy_migration -> canonical schemas -> repositories
- API routers must not execute SQL or call providers directly.
- A use-case router must never import another router or the composed
api_router. - Shared persistence is accessed through repository contracts, not through another API endpoint.
router.pyis composition-only: removing one included router must not break imports or startup for the others.- Repositories must not import FastAPI or provider clients.
- Provider adapters return canonical schemas; services do not parse vendor payloads.
- Workflows own allowed stage transitions, required conditions, and audit events.
- Legacy migration code may write only through staging/repository interfaces and must never update the source system.
- SQLAlchemy models must not be returned directly from public APIs.
Each versioned router is independently mountable on a minimal FastAPI application. This keeps transport concerns isolated and allows a use case to be extracted into its own deployment later without rewriting its HTTP contract.
| Use case | Router module | Service/repository dependency | Imports another router |
|---|---|---|---|
| Applications | api/v1/applications.py |
Demonstration application data | No |
| Audit ingestion | api/v1/audit.py |
Audit schema | No |
| Portfolio | api/v1/portfolio.py |
Portfolio calculation | No |
| Migration validation | api/v1/migration_manifests.py |
Legacy-manifest validator | No |
| Application intelligence | api/v1/ai.py |
AI assessment service | No |
| Document storage | api/v1/documents.py |
S3 storage adapter | No |
| User management | api/v1/user_management.py |
User management service and repository contract | No |
| Password recovery | api/v1/password_recovery.py |
Password recovery service, independent email/SMS adapters, and repository contract | No |
backend/tests/test_router_isolation.py and the standalone cases in backend/tests/test_users.py mount these routers one at a time, call their primary endpoint, and verify that unrelated routes are not present.
- All business endpoints use
/api/v1. - Additive response fields are backward compatible.
- Removing/renaming fields, changing meaning, or changing required inputs requires
/api/v2. - Provider and database schema versions are independent from API versions.
- Require authenticated actor, tenant/entity, role, and correlation ID.
- Require an
Idempotency-Keyfor create, payment, provider-pull, sanction, and disbursement commands. - Validate consent and permissible purpose before regulated provider requests.
- Use optimistic concurrency (
record_versionor ETag) for updates.
{
"error": {
"code": "APPLICATION_STAGE_CONFLICT",
"message": "Application cannot move from Documents to Sanctioned",
"correlation_id": "uuid",
"details": []
}
}Do not return stack traces, SQL errors, provider credentials, or raw vendor payloads.
Every request should emit structured logs and metrics containing correlation ID, route, status, duration, tenant, actor/service identity, and non-sensitive entity identifiers. PII and provider payload logging is disabled by default.
- One API command owns one database transaction.
- External provider calls do not remain inside an open database transaction.
- Use an outbox for reliable events/tasks after commit.
- Use inbox/idempotency records for provider webhooks and retried commands.
- Audit events are written in the same transaction as the business state change where possible.
The current application endpoints still use fixtures.py and user_fixtures.py. The new models and migration foundation establish the target persistence contract; repositories and production authentication are the next implementation step.