20. Backend Repository Structure¶
20.1 Overview¶
The Identity Platform backend follows hexagonal architecture (ports and adapters) with FastAPI and Python.
20.2 Directory Structure¶
identity-platform/
├── src/
│ ├── main.py # FastAPI app entrypoint
│ ├── config/
│ │ ├── __init__.py
│ │ ├── settings.py # Pydantic settings
│ │ └── constants.py # App constants
│ │
│ ├── domain/ # Core business logic (no external deps)
│ │ ├── __init__.py
│ │ ├── entities/
│ │ │ ├── __init__.py
│ │ │ ├── user.py # User entity
│ │ │ ├── identity.py # Identity (email, phone, social)
│ │ │ ├── kyc.py # KYC entity
│ │ │ ├── device.py # Device entity
│ │ │ ├── challenge.py # Challenge entity
│ │ │ ├── consent.py # Consent entity
│ │ │
│ │ ├── value_objects/
│ │ │ ├── __init__.py
│ │ │ ├── user_id.py
│ │ │ ├── mobile.py
│ │ │ ├── email.py
│ │ │ ├── kyc_level.py
│ │ │ ├── kyc_status.py
│ │ │ └── palm_template.py
│ │ │
│ │ ├── services/
│ │ │ ├── __init__.py
│ │ │ ├── auth_service.py # Auth business logic
│ │ │ ├── user_service.py # User business logic
│ │ │ ├── kyc_service.py # KYC business logic
│ │ │ ├── enrollment_service.py
│ │ │ ├── verification_service.py
│ │ │ └── consent_service.py
│ │ │
│ │ ├── events/
│ │ │ ├── __init__.py
│ │ │ ├── user_events.py # Domain events
│ │ │ ├── kyc_events.py
│ │ │ └── enrollment_events.py
│ │ │
│ │ └── exceptions/
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ ├── user.py
│ │ ├── kyc.py
│ │ └── enrollment.py
│ │
│ ├── ports/ # Interfaces (abstractions)
│ │ ├── __init__.py
│ │ ├── inbound/ # Driving ports
│ │ │ ├── __init__.py
│ │ │ ├── auth_port.py
│ │ │ ├── user_port.py
│ │ │ ├── kyc_port.py
│ │ │ ├── enrollment_port.py
│ │ │ └── device_port.py
│ │ │
│ │ └── outbound/ # Driven ports
│ │ ├── __init__.py
│ │ ├── user_repository.py
│ │ ├── kyc_repository.py
│ │ ├── device_repository.py
│ │ ├── challenge_repository.py
│ │ ├── consent_repository.py
│ │ ├── kyc_provider.py # KYC provider interface
│ │ ├── palm_verifier.py # Palm verification interface
│ │ ├── otp_sender.py # OTP interface
│ │ ├── email_sender.py # Email interface
│ │ ├── event_publisher.py # Event publishing interface
│ │ └── token_store.py # Refresh token store
│ │
│ ├── adapters/ # Implementations
│ │ ├── __init__.py
│ │ ├── inbound/ # Driving adapters (API)
│ │ │ ├── __init__.py
│ │ │ ├── rest/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── auth_router.py
│ │ │ │ ├── user_router.py
│ │ │ │ ├── kyc_router.py
│ │ │ │ ├── enrollment_router.py
│ │ │ │ ├── challenge_router.py
│ │ │ │ ├── device_router.py
│ │ │ │ ├── consent_router.py
│ │ │ │ └── health_router.py
│ │ │ │
│ │ │ ├── middleware/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── auth_middleware.py
│ │ │ │ ├── tenant_middleware.py
│ │ │ │ └── logging_middleware.py
│ │ │ │
│ │ │ └── schemas/ # Pydantic request/response
│ │ │ ├── __init__.py
│ │ │ ├── auth.py
│ │ │ ├── user.py
│ │ │ ├── kyc.py
│ │ │ ├── enrollment.py
│ │ │ ├── challenge.py
│ │ │ ├── device.py
│ │ │ └── consent.py
│ │ │
│ │ └── outbound/ # Driven adapters
│ │ ├── __init__.py
│ │ ├── persistence/
│ │ │ ├── __init__.py
│ │ │ ├── postgres/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── models.py # SQLAlchemy models
│ │ │ │ ├── user_repository.py
│ │ │ │ ├── kyc_repository.py
│ │ │ │ ├── device_repository.py
│ │ │ │ ├── challenge_repository.py
│ │ │ │ └── consent_repository.py
│ │ │ │
│ │ │ └── redis/
│ │ │ ├── __init__.py
│ │ │ ├── token_store.py
│ │ │ └── otp_store.py
│ │ │
│ │ ├── kyc_providers/
│ │ │ ├── __init__.py
│ │ │ ├── nafath_adapter.py
│ │ │ ├── onfido_adapter.py
│ │ │ ├── sumsub_adapter.py
│ │ │ └── mock_adapter.py # For testing
│ │ │
│ │ ├── palm_providers/
│ │ │ ├── __init__.py
│ │ │ ├── biowave_adapter.py # X-Telcom BioWave Pass palm verification
│ │ │ └── mock_adapter.py # For testing
│ │ │
│ │ ├── social/
│ │ │ ├── __init__.py
│ │ │ ├── google_adapter.py
│ │ │ └── apple_adapter.py
│ │ │
│ │ ├── messaging/
│ │ │ ├── __init__.py
│ │ │ ├── twilio_sms.py
│ │ │ ├── unifonic_sms.py
│ │ │ └── sendgrid_email.py
│ │ │
│ │ └── events/
│ │ ├── __init__.py
│ │ ├── kafka_publisher.py
│ │ └── webhook_publisher.py
│ │
│ ├── core/ # Shared utilities
│ │ ├── __init__.py
│ │ ├── security/
│ │ │ ├── __init__.py
│ │ │ ├── jwt.py # JWT handling
│ │ │ ├── jwks.py # JWKS generation
│ │ │ ├── password.py # Password hashing
│ │ │
│ │ ├── database.py # DB connection
│ │ ├── redis.py # Redis connection
│ │ ├── logging.py # Structured logging
│ │ └── utils.py
│ │
│ └── di/ # Dependency injection
│ ├── __init__.py
│ ├── container.py # DI container
│ └── providers.py # Dependency providers
│
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Pytest fixtures
│ ├── unit/
│ │ ├── domain/
│ │ │ ├── test_auth_service.py
│ │ │ ├── test_user_service.py
│ │ │ └── test_kyc_service.py
│ │ │
│ │ └── adapters/
│ │ ├── test_nafath_adapter.py
│ │ └── test_biowave_adapter.py
│ │
│ ├── integration/
│ │ ├── test_auth_flow.py
│ │ ├── test_kyc_flow.py
│ │ └── test_enrollment_flow.py
│ │
│ └── e2e/
│ └── test_full_onboarding.py
│
├── migrations/ # Alembic migrations
│ ├── versions/
│ ├── env.py
│ └── alembic.ini
│
├── scripts/
│ ├── generate_jwks.py
│ └── seed_tenants.py
│
├── docker/
│ ├── Dockerfile
│ ├── Dockerfile.dev
│ └── docker-compose.yml
│
├── k8s/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml
│ └── secrets.yaml
│
├── .env.example
├── .gitignore
├── pyproject.toml
├── poetry.lock
├── Makefile
└── README.md
20.3 Key Architecture Principles¶
1. Hexagonal Architecture (Ports & Adapters)
┌─────────────────────────────────────────────────────────────┐
│ Adapters (Inbound) │
│ REST API │ gRPC │ CLI │ Message Consumer │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Ports (Inbound) │
│ AuthPort │ UserPort │ KYCPort │ EnrollmentPort │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Domain Services │
│ AuthService │ UserService │ KYCService │ EnrollmentService│
│ │
│ Entities: User, Identity, KYC, Device, Challenge │
│ Value Objects: UserId, Mobile, Email, KYCLevel │
│ Domain Events: UserCreated, KYCVerified │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Ports (Outbound) │
│ UserRepository │ KYCProvider │ PalmVerifier │ OTPSender │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Adapters (Outbound) │
│ PostgreSQL │ Redis │ Nafath │ X-Telcom BioWave Pass │ Twilio │ Kafka │
└─────────────────────────────────────────────────────────────┘
2. Domain-Driven Design
- Entities have identity and lifecycle
- Value objects are immutable
- Domain services contain business logic
- Domain events for decoupling
3. Dependency Injection
The application uses a declarative dependency injection container to wire all components together:
- Repositories are registered as singletons (e.g.,
UserRepositorybacked by PostgreSQL) and injected into services that need data access. - KYC Provider is resolved dynamically via a selector based on the tenant's configured provider name (e.g.,
nafath,onfido,mock). Each provider adapter is instantiated as a singleton with its own configuration. - Palm Verifier is resolved dynamically via a selector based on the tenant's configured palm vendor (e.g.,
biowave,mock). Each vendor adapter is instantiated as a singleton with its own configuration — identical pattern to KYC providers. - Domain Services (e.g.,
KYCService,EnrollmentService) receive their dependencies — repositories, provider/vendor adapters, and event publishers — through constructor injection, keeping them decoupled from concrete implementations.
4. Clean Separation
| Layer | Can depend on | Cannot depend on |
|---|---|---|
| Domain | Nothing external | Adapters, frameworks |
| Ports | Domain | Adapters |
| Adapters | Ports, Domain | Other adapters |
20.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 its adapters (Nafath, Onfido, Sumsub) follow the same ports-&-adapters pattern as PalmVerifier below, but the detailed port interface and adapter behavior are deferred until KYC is architected — see §7.
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.
20.5 Testing Strategy¶
| Type | Location | Purpose |
|---|---|---|
| Unit | tests/unit/ |
Test domain logic in isolation |
| Integration | tests/integration/ |
Test adapters with real deps |
| E2E | tests/e2e/ |
Full flow testing |