Skip to content

17. Backend Repository Structure

17.1 Overview

The Identity Platform backend is a modular (vertical-slice) hexagon built with FastAPI and Python. Each bounded context is its own module — with its own domain, use-cases, ports, and adapters — while cross-cutting concerns live in core/, bootstrap/, and shared/. A React admin console ships alongside in web/.

17.2 Directory Structure

link-identity-backend/
├── app/
│   ├── main.py                # app factory + router wiring
│   ├── run.py                 # dev entrypoint
│   ├── bootstrap/             # TLS / mTLS runtime (device client-cert serving)
│   ├── core/                  # config, db, security, jwks, middleware,
│   │                          #   rate_limit, schemas, exceptions, logging, sentry
│   ├── shared/models/         # SQLAlchemy ORM models (users, devices, tenants, …)
│   ├── modules/               # one hexagon per bounded context (15, listed below)
│   └── tests/                 # unit/, integration/ (+ conftest, db helpers)
├── alembic/ , alembic.ini     # migrations
├── docker-compose.yml , docker-compose.prod.yml , Dockerfile
├── requirements.txt , pytest.ini , specs/
├── web/                       # React + Vite + Tailwind admin console
└── README.md

app/modules/ holds 15 bounded contexts: auth, user, palm, devices, enrollment, kyc, consent, linked_services, transactions, webhook, ssl, client, console, admin, test. Each follows the same shape:

{context}/
├── di.py                      # wires this module's ports → adapters
├── domain/                    # entities, pure services (rules), exceptions
├── application/               # use-cases (orchestration)
├── ports/
│   ├── inbound/               # request/response contracts (schemas)
│   └── outbound/              # driven interfaces (repositories, vendor clients)
└── adapters/
    ├── inbound/router.py      # FastAPI routes
    └── outbound/repository.py # persistence + external vendor clients

One deliberate split: domain entities are per-module (domain/entities.py), while SQLAlchemy ORM models are centralized in app/shared/models/ — the persistence schema sits in one place, the business rules sit with their context.

17.3 Key Architecture Principles

1. Modular hexagon. Each module is a self-contained hexagon: domain at the centre, then application (use-cases), then ports (interfaces), with adapters on the edge. Modules talk to one another only through interfaces — never by reaching into another module's internals.

2. Layered dependency rule (within a module):

Layer May depend on May not depend on
Domain nothing external application, adapters, frameworks
Application domain, ports adapters
Adapters ports, domain other modules' internals

3. Vendor-pluggable ports. External vendors sit behind outbound ports with swappable adapters — PalmVerifier (X-Telcom BioWave Pass, mock) and KYCProvider (Nafath / Onfido / Sumsub — [POST-MVP]). The adapter is chosen per tenant from configuration.

4. Dependency injection. Each module wires its own ports to adapters in di.py; core/ supplies shared infrastructure (database, security, config). Repositories are singletons; vendor adapters are resolved per tenant from the configured provider name.

5. Cross-cutting homes. core/ holds config, database, security/JWT/JWKS, middleware, rate limiting, error handling, logging, and Sentry. bootstrap/ sets up TLS and the mTLS client-certificate path for device authentication (§8). shared/models/ holds the ORM schema.

17.4 Key Files

Domain Entity Example — User:

The User entity contains the following fields:

Field Type Description
id string Unique user identifier
tenant_id string Owning tenant
identities list of Identity Linked auth identities (email, phone, social)
profile object (optional) User profile data
kyc KYCData (optional) KYC verification data
palm_enrolled boolean Whether palm biometric is enrolled
status string active, suspended, or deleted
created_at timestamp Account creation time
updated_at timestamp Last modification time

The entity exposes business logic methods: - is_kyc_verified() — returns true if the user has completed KYC with a verified status. - can_enroll_palm(require_kyc) — returns true if the user has not already enrolled and, when KYC is required by tenant config, has a verified KYC status.

Port Example — KYC Provider & adapters — [POST-MVP]:

The KYCProvider port and adapters (Nafath, Onfido, Sumsub) are deferred until KYC is architected — see §6.

Port Example — PalmVerifier:

The PalmVerifier port defines the contract that every palm verification vendor adapter must fulfill. Any adapter (X-Telcom BioWave Pass, or a future vendor) must implement:

Method Purpose
enroll(user_id, palm_template) Store a palm template for a user at the vendor
verify(user_id, palm_scan) 1:1 match: compare scan against user's stored template
identify(palm_scan, tenant_id) 1:N search: find matching user across all templates
get_status(user_id) Check if user has an enrolled template
delete_template(user_id) Remove a stored palm template from the vendor

Adapter Example — X-Telcom BioWave Pass:

The BioWaveAdapter implements the PalmVerifier port for X-Telcom BioWave Pass's palm vein verification service. It is initialized with vendor-specific configuration (base_url, request_id, timeout_ms). When enroll is called, the adapter:

  1. Sends an HTTP POST to the X-Telcom BioWave Pass enrollment API with the user reference and palm template data.
  2. Maps the X-Telcom BioWave Pass response into a standard EnrollmentResult domain entity (with user_id, status, vendor: "biowave", and timestamp).

For verify (1:1) and identify (1:N), the adapter submits the palm scan to the appropriate X-Telcom BioWave Pass endpoint and maps the response into VerificationResult or IdentificationResult domain entities including confidence scores and latency metrics.

This pattern is identical for any future palm adapter — each translates between the external vendor's API and the platform's domain entities.

17.5 Testing

Type Location Purpose
Unit app/tests/unit/ domain + application logic in isolation
Integration app/tests/integration/ adapters against real dependencies

Shared fixtures live in app/tests/conftest.py, with database helpers in database.py and db_support.py.