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:
- Sends an HTTP POST to the X-Telcom BioWave Pass enrollment API with the user reference and palm template data.
- Maps the X-Telcom BioWave Pass response into a standard
EnrollmentResultdomain entity (withuser_id,status,vendor: "biowave", andtimestamp).
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.