Aller au contenu

Architecture technique

Version : 1.0 Date : 22 avril 2026 Auteur : Chaouki Barkia Documents liés :


  1. Objectifs du document et hors-périmètre
  2. Principes d’architecture
  3. Vue macro de la plateforme
  4. Catalogue des services
  5. Modèle de données
  6. API publique
  7. Parcours critiques — diagrammes de séquence
  8. Gestion des licences modulaires
  9. Déploiement SaaS vs On-Premise
  10. Sécurité détaillée
  11. Observabilité et SLOs
  12. Capacity planning et sizing
  13. Exemple concret — générateur FATCA XML v2.0
  14. Plan de test et qualité
  15. Annexes

Ce document traduit le cahier des charges fonctionnel en une architecture technique actionnable : catalogue de services, contrats d’API, modèle de données, parcours, topologie de déploiement. Il vise à servir de référence pour l’équipe d’ingénierie dès le kickoff du MVP.

Hors périmètre (à produire ultérieurement) :

  • Plan de test détaillé par cas (fichier QA dédié) ;
  • Modèles ML et jeux d’entraînement OCR / liveness ;
  • Guides ops (runbooks par incident) ;
  • Dossier conformité audit (ISO 27001, SOC 2) ;
  • Code source (le présent document décrit, ne code pas — sauf extraits illustratifs §13).

Les choix de stack rappelés dans ce document proviennent de la section 6 du cahier des charges. Toute divergence ultérieure doit être tracée par un ADR (Architecture Decision Record) dans le dépôt code.


#PrincipeImplication
1Un codebase, deux modesSaaS et on-prem s’appuient sur la même image OCI et le même Helm chart ; la différence est dans les values.
2API-firstChaque capacité métier est exposée par API interne avant toute UI. La console back-office est un client API comme un autre.
3Event-driven pour les traitements asyncKafka transporte les événements métier (document uploadé, décision KYC, alerte AML). Les services consomment ce qui les concerne.
4Stateless services + état externaliséChaque service est scalable horizontalement ; l’état persiste dans PostgreSQL, Redis, MinIO, Kafka.
5Isolation tenant par défauttenant_id en clé logique partout, clé de chiffrement dédiée par tenant, row-level security PostgreSQL.
6Sécurité par défautmTLS service-à-service, OIDC pour utilisateurs, chiffrement au repos et en transit, secrets via Vault.
7Observabilité nativeChaque service émet traces OpenTelemetry, métriques Prometheus, logs JSON structurés.
8Pas de dépendance propriétaire cloudTout composant a un équivalent OSS utilisable on-prem (MinIO pour S3, Vault pour KMS, Keycloak pour IAM).
9Composants vétustes remplaçablesLes contrats entre services sont versionnés ; remplacer un service (ex : OCR engine) n’impacte pas les autres.
10Mesure du coût unitaireChaque vérification porte une étiquette de coût (cost_unit) remontée aux analytics pour calculer la marge par check.


ServiceResponsabilitéLangageDépendancesCriticité
identity-svcGestion users, tenants, rôles, clés API, consentements ; intégration KeycloakKotlin / Spring BootPostgreSQL, KeycloakP0
kyc-svcOrchestration d’un dossier KYC (individu + entreprise), état, décisionKotlin / Spring BootPostgreSQL, Kafka, TemporalP0
aml-svcScreening sanctions/PEP/adverse media, ongoing monitoring, risk scoringKotlin / Spring BootPostgreSQL, OpenSearch, Kafka, TemporalP0
tcr-svcTax Compliance & Reporting : classification, formulaires, reporting FATCA/CRSKotlin / Spring BootPostgreSQL, MinIO, KafkaP1 (P0 pour clients banque)
workflow-svcMoteur de règles d’orchestration no-code + états de workflow configurablesKotlin / Spring BootPostgreSQL, TemporalP0
case-svcCase management back-office : queue agent, assignation, commentaires, SLAKotlin / Spring BootPostgreSQL, KafkaP1
audit-svcJournal d’audit immutable (WORM + hash chain) ; export SIEMKotlin / Spring BootPostgreSQL + table append-only, OpenSearchP0
report-svcGénération XML FATCA v2.0, XML CRS, rapports PDF, exportsKotlin / Spring BootPostgreSQL, MinIOP1
ServiceResponsabilitéLangageModèle de déploiement
ocr-svcOCR documents (passeport, CNI, justificatifs) ; arabe + latin + hindiPython / FastAPI / ONNX RuntimeCPU par défaut, GPU optionnel (auto-scaling sur demande)
liveness-svcDétection de vie (passive niveau 2, fallback actif)Python / FastAPI / ONNX RuntimeGPU recommandé en SaaS, CPU suffisant en on-prem faible volume
face-match-svcComparaison biométrique faciale document ↔ selfiePython / FastAPI / ONNX RuntimeCPU (latence tolérable)
voice-svc (V2)Biométrie vocalePython / FastAPIGPU
nlp-aml-svcNLP adverse media (classification, extraction)Python / FastAPIGPU pour fine-tuning, CPU pour inference
ServiceResponsabilité
doc-storeWrapper stockage fichiers avec chiffrement per-tenant (KMS), signed URLs, retention, legal hold
webhook-dispatcherSignature HMAC, retries exponentiels, dead-letter queue, replay manuel
license-svcVérification clé Ed25519, feature flags, rate limiting par licence, phone-home on-prem optionnel
notification-svcEmail (SMTP externalisé), SMS (provider au choix), push mobile, templates par tenant
connector-rneAdaptateur RNE Tunisie (services KYC + UBO) avec cache Redis, fallback, circuit breaker
connector-idesAdaptateur IDES/DGI Tunisie (dépôt FATCA, récupération AD/RE)
connector-dataAdaptateurs data enrichment (World-Check, Dow Jones, D&B…) via couche d’abstraction
ComposantChoixJustification
API GatewayKongPlugins matures, OSS, runtime léger. Alternative : Envoy.
Service mesh (mTLS)LinkerdPlus léger qu’Istio, mTLS natif, observabilité native.
Bus événementsKafka (SaaS) / Redpanda (on-prem léger)Exactly-once, partitionnement par tenant, retention pour audit.
Workflow engineTemporal.ioDurabilité, retry natif, long-running (ongoing monitoring AML, revalidation FATCA à 3 ans).
Cache & locksRedis 7 (ou Valkey OSS)Pub/Sub, distributed locks (Redlock), rate limiting.
RechercheOpenSearchFull-text multilingue (arabe), analytics dashboards, SIEM export.
IAMKeycloakOIDC, SAML, SCIM, portable cloud/on-prem.
SecretsHashiCorp VaultKV v2, dynamic secrets DB, PKI interne, rotation auto.
Object storageS3 API (AWS S3 / GCS / MinIO on-prem)Portabilité totale.
CI/CDGitHub Actions + ArgoCDGitOps, auditabilité.

  • Modèle choisi : shared schema, row-level isolation via colonne tenant_id obligatoire sur toute table métier + Row Level Security (RLS) PostgreSQL activée.
  • Clé de chiffrement par tenant (colonne tenant_encryption_key_id → HashiCorp Vault Transit engine). Les champs PII (documents, données de naissance, TIN) sont chiffrés côté application.
  • Isolation renforcée pour clients Enterprise / Regulated : schémas dédiés par tenant (option), voire base dédiée.
  • On-prem : la notion de tenant est conservée (pour une future extension) mais dégénérée à un seul tenant actif.
table: tenant
tenant_id UUID PK
code VARCHAR(64) UNIQUE
name VARCHAR(255)
deployment_mode ENUM('saas_shared', 'saas_dedicated', 'on_premise')
residency_region VARCHAR(32) -- 'eu-west', 'mena', ...
encryption_key_id VARCHAR(128) -- ref Vault Transit
license_id UUID FK
status ENUM('active', 'suspended', 'terminated')
created_at, updated_at
table: user
user_id UUID PK
tenant_id UUID FK
email CITEXT
keycloak_subject VARCHAR(64) -- sub OIDC
role ENUM('agent','supervisor','admin','auditor','api_client')
status ENUM('active','invited','disabled')
last_login_at
created_at, updated_at
UNIQUE(tenant_id, email)
table: api_key
api_key_id UUID PK
tenant_id UUID FK
key_hash BYTEA -- argon2id
scopes TEXT[] -- ex: kyc.read, tcr.write
expires_at TIMESTAMPTZ
created_by UUID FK user
created_at, revoked_at
table: kyc_case
case_id UUID PK
tenant_id UUID FK
external_ref VARCHAR(128) -- reference client
subject_type ENUM('individual','business')
status ENUM('created','in_progress','auto_approved',
'manual_review','approved','rejected','expired')
decision_reason TEXT
risk_score NUMERIC(5,2)
opened_at, closed_at
expires_at TIMESTAMPTZ -- validité KYC (réglementaire)
workflow_instance_id VARCHAR(128) -- Temporal workflow ID
created_at, updated_at
table: kyc_individual
case_id UUID PK FK
first_name BYTEA -- chiffré
last_name BYTEA -- chiffré
dob BYTEA -- chiffré
nationality CHAR(2)
place_of_birth_country CHAR(2)
-- indicia FATCA (dérivés pour scoring rapide) :
fatca_indicia_count SMALLINT
-- ...
table: kyc_business
case_id UUID PK FK
tenant_id UUID FK
-- identifiants
rne_id VARCHAR(16) -- identifiantUnique RNE (7 chiffres + 1 lettre)
registration_number VARCHAR(64) -- num_REGISTRE_INTERNE RNE ou équivalent
registration_country CHAR(2)
-- dénominations (bilingue)
legal_name_fr VARCHAR(255) -- denomination_FR
legal_name_ar VARCHAR(255) -- denomination_AR
trade_name_fr VARCHAR(255) -- nom_COMMERCIAL_FR
trade_name_ar VARCHAR(255)
brand_fr VARCHAR(255) -- enseigne_FR
brand_ar VARCHAR(255)
-- structure légale
legal_form_fr VARCHAR(150) -- forme_JURIDIQUE_FR
legal_form_ar VARCHAR(150)
entity_type CHAR(1) -- 'P' (Physique) | 'M' (Morale)
category VARCHAR(32) -- A/S/E (PM) ; A/C/M (PP)
auto_entrepreneur BOOLEAN
-- capital
capital_amount NUMERIC(30,3)
capital_currency CHAR(3) DEFAULT 'TND'
last_balance_year SMALLINT -- dernier_BILAN_DEPOSE
-- statut
status VARCHAR(32) -- mappé : active | suspended | closed (depuis NORMAL / RADIE_PROVISOIREMENT / RADIE_DEFINITIVEMENT)
date_start_activity DATE
date_registration DATE -- date_ENREGISTREMENT_RNE
date_publication DATE
date_radiation DATE
date_last_update DATE -- date_DERNIERE_MAJ
-- adresses (siège et activité)
address_head JSONB -- {gouvernorat, ville, rue, code_postal} × {ar, fr}
address_activity JSONB
-- activités NAT 1996
primary_activity JSONB -- {codeActivite, detailActiviteArabe, detailActiviteLatin}
secondary_activities JSONB
-- UBO déclarés
has_ubo_declaration BOOLEAN -- declaration_BENEFICIAIRES_EFFECTIFS
-- métadonnées fournisseur
source VARCHAR(32) -- 'RNE_WS_KYC' | 'MANUAL' | 'INFOGREFFE' | ...
source_fetched_at TIMESTAMPTZ
raw_payload JSONB -- payload brut pour audit
table: kyc_business_officer -- direction (dirigeants)
officer_id UUID PK
case_id UUID FK
rne_gestion_id VARCHAR(16) -- IDENTIFIANT_UNIQUE_GESTION
national_id VARCHAR(40) -- NUM_PIECE_IDENTITE_GESTION
national_id_date DATE
first_name_fr VARCHAR(100)
first_name_ar VARCHAR(100)
last_name_fr VARCHAR(100)
last_name_ar VARCHAR(100)
full_name_fr VARCHAR(255) -- NOM_PRENOM_GESTION_FR (pour matching)
full_name_ar VARCHAR(255)
gender CHAR(2)
nationality_fr VARCHAR(50)
nationality_ar VARCHAR(50)
dob DATE
birth_country_fr VARCHAR(100)
birth_country_ar VARCHAR(100)
role_fr VARCHAR(100) -- QUALITE_GESTION_FR (Commissaire aux Comptes, Directeur Général, ...)
role_ar VARCHAR(100)
is_statutory BOOLEAN -- STATUTAIRE
nomination_from DATE -- DATE_NOMINATION
nomination_to DATE -- DATE_FIN_NOMINATION
address JSONB
powers JSONB -- pouvoirs[{libelle_fr, libelle_ar}]
-- AML
aml_subject_id UUID -- lien vers aml_subject pour screening
table: kyc_business_ubo -- bénéficiaires effectifs (WS-BE)
ubo_id UUID PK
case_id UUID FK
declaration_number VARCHAR(40) -- NUM_DECLARATION
type VARCHAR(32) -- 'detention_capital' | 'gestion' | 'pouvoir'
full_name_fr VARCHAR(255) -- NP_PERSONNE_FR
full_name_ar VARCHAR(255)
nationality VARCHAR(50)
national_id VARCHAR(40) -- NID_PERSONNE
resident BOOLEAN -- RESIDENT : true si Résident
capital_direct NUMERIC(10,3) -- % détenu directement
capital_indirect NUMERIC(10,3)
voting_rights NUMERIC(10,3) -- DROIT_VOTE
declaration_date DATE -- DATE_DECLARATION
beneficiary_start DATE -- DATE_DEBUT_ETRE_BENEFICIAIRE
status VARCHAR(16) -- 'ACTIF' | 'INACTIF'
-- AML
aml_subject_id UUID
table: rne_subscription -- gestion des abonnements RNE par tenant
subscription_id SERIAL PK
tenant_id UUID FK
rne_subscription_id INT -- fourni par le RNE
rne_subscriber_id INT -- fourni par le RNE
rne_code_motif INT -- motif WS-BE
bearer_token_ref VARCHAR(128) -- ref Vault
outbound_ip INET -- IP déclarée au RNE
balance_remaining INT -- cache informatif
active BOOLEAN
created_at, updated_at
table: kyc_document
document_id UUID PK
case_id UUID FK
type ENUM('passport','id_card','drivers_license',
'proof_address','trade_license','rne_extract', ...)
object_key VARCHAR(512) -- clé MinIO/S3 (chiffré SSE-KMS)
ocr_status ENUM('pending','done','failed')
ocr_payload JSONB -- champs extraits
verification_status ENUM('pending','valid','invalid','suspicious')
created_at
table: kyc_biometric_check
check_id UUID PK
case_id UUID FK
kind ENUM('liveness_passive','liveness_active','face_match','voice')
score NUMERIC(5,4)
passed BOOLEAN
details JSONB
performed_at
table: aml_subject
subject_id UUID PK
tenant_id UUID FK
case_id UUID FK kyc_case
full_name_norm VARCHAR(512) -- normalisé (translittération, minuscules)
dob DATE
nationality CHAR(2)
status ENUM('clear','alert','blocked')
last_screened_at
table: aml_screening
screening_id UUID PK
subject_id UUID FK
screened_at TIMESTAMPTZ
sources TEXT[] -- ex: OFAC, UN, EU, HMT, BCT, adverse_media
hits_count INT
decision ENUM('auto_clear','manual_review','escalate','block')
table: aml_hit
hit_id UUID PK
screening_id UUID FK
list_source VARCHAR(64)
list_record_id VARCHAR(128)
match_score NUMERIC(5,4)
match_type ENUM('sanction','pep','adverse_media','rca')
raw_payload JSONB
table: aml_monitoring_subscription
subscription_id UUID PK
subject_id UUID FK
frequency ENUM('daily','weekly','on_list_update')
active BOOLEAN
last_run_at
table: aml_alert
alert_id UUID PK
tenant_id UUID FK
subject_id UUID FK
triggered_at TIMESTAMPTZ
kind ENUM('new_sanction_hit','pep_update','adverse_media_new', ...)
severity ENUM('low','medium','high','critical')
status ENUM('open','assigned','resolved','false_positive')
assignee_user_id UUID FK user
resolved_at
table: tcr_classification
classification_id UUID PK
tenant_id UUID FK
case_id UUID FK kyc_case
us_person BOOLEAN -- FATCA
fatca_category VARCHAR(16) -- ex: FATCA102 (EENF passive)
holder_type ENUM('Individual','Organisation')
tax_residences JSONB -- [{country, tin, certified_at, expires_at}]
indicia JSONB -- détail indicia détectés
classified_at
classified_by ENUM('auto','manual')
table: tcr_form
form_id UUID PK
tenant_id UUID FK
case_id UUID FK
type ENUM('W-8BEN','W-8BEN-E','W-9')
version VARCHAR(16) -- ex: rev_oct_2021
prefilled_payload JSONB
pdf_object_key VARCHAR(512)
signed_object_key VARCHAR(512) -- post e-signature
signed_at TIMESTAMPTZ
expires_at TIMESTAMPTZ -- typiquement +3 ans
status ENUM('draft','sent','signed','expired','revoked')
table: tcr_declaration -- fichier FATCA XML v2.0 généré
declaration_id UUID PK
tenant_id UUID FK
reporting_fi_giin VARCHAR(32) -- GIIN de l'IF déclarante
reporting_period VARCHAR(4) -- exercice (ex: 2025)
type ENUM('FATCA1','FATCA2','FATCA3','FATCA4','NilReport')
message_ref_id VARCHAR(200) UNIQUE
corr_message_ref_id VARCHAR(200) -- chaînage
xml_object_key VARCHAR(512)
file_hash_sha256 CHAR(64)
generated_at TIMESTAMPTZ
submitted_at TIMESTAMPTZ -- dépôt IDES
ides_reference VARCHAR(128) -- référence DGI
status ENUM('draft','generated','submitted','accepted',
'rejected','cancelled')
table: tcr_account_report
account_report_id UUID PK
declaration_id UUID FK
doc_ref_id VARCHAR(200) UNIQUE
corr_doc_ref_id VARCHAR(200)
account_number VARCHAR(128)
account_number_type VARCHAR(16) -- OECD601..605, NANUM
account_balance NUMERIC(18,2)
account_balance_ccy CHAR(3)
account_closed BOOLEAN
holder_type VARCHAR(8) -- FATCA102/103/104
holder_subject_id UUID -- lien KYC
payloads JSONB -- AdditionalData
table: tcr_ides_acknowledgement
ack_id UUID PK
declaration_id UUID FK
kind ENUM('deposit','error_report','irs_notification')
received_at TIMESTAMPTZ
payload JSONB
irs_error_code VARCHAR(32) -- ex: 8008, 8009, 8011 (ICMM)
actionable BOOLEAN
table: audit_event
event_id UUID PK
tenant_id UUID FK
actor_type ENUM('user','system','api_client','agent')
actor_id VARCHAR(128)
action VARCHAR(64) -- ex: KYC_CASE.DECISION_MADE
target_type VARCHAR(64)
target_id VARCHAR(128)
payload JSONB
occurred_at TIMESTAMPTZ
previous_hash BYTEA -- chaînage
event_hash BYTEA -- SHA-256(previous_hash || event_data)
-- Partitionné par mois, table INSERT-only, policy Postgres empêchant UPDATE/DELETE
  • Index composite (tenant_id, <colonne business>) sur toutes les tables métier (discrimination tenant toujours en premier).
  • Index partiel sur statuts actifs : WHERE status IN ('in_progress','manual_review').
  • Index GIN sur JSONB payload et indicia pour recherche ad hoc.
  • Pour OpenSearch : index aml_hits_{tenant_id} miroir du matching fuzzy multilingue.

  • REST + OpenAPI 3.1 en première intention ; endpoints asynchrones exposent des webhooks.
  • Versioning par URL : /v1/…. Un breaking change → /v2/…, cohabitation 12 mois minimum.
  • Authentification : Authorization: Bearer <jwt> (OIDC) ou X-API-Key: <key> pour intégrations machine.
  • Multi-tenant implicite : le token porte le tenant_id — jamais dans le path.
  • Idempotence : header Idempotency-Key obligatoire sur tous les POST mutatifs, TTL 24 h.
  • Rate limiting : par clé API, configurable par tenant (défaut : 100 rps burst, 20 rps sustained).
  • Webhooks signés : header X-VitaKYC-Signature: t=<ts>,v1=<hmac>, rotation des secrets possible sans interruption.
  • Pagination cursor-based (?cursor=…&limit=50) ; jamais d’offset/limit sur des gros volumes.
  • Format d’erreur : RFC 9457 (Problem Details).

Les exemples ci-dessous illustrent le style ; un spec OpenAPI complet est produit en parallèle dans openapi/vitakyc.yaml.

POST /v1/kyc/cases
Content-Type: application/json
Authorization: Bearer <jwt>
Idempotency-Key: 7d3e9c2a-...
{
"external_ref": "CLIENT-2026-00123",
"subject_type": "individual",
"locale": "fr-TN",
"workflow_code": "default-tn-v1",
"initial_data": {
"email": "client@example.tn",
"phone": "+21698XXXXXX"
},
"tags": ["retail", "tn-residence"]
}
201 Created
{
"case_id": "c9e1...",
"status": "created",
"embed_url": "https://kyc.vitakyc.io/s/eyJhbGci...",
"expires_at": "2026-04-23T14:30:00Z"
}
POST /v1/aml/screen
Content-Type: application/json
Authorization: Bearer <jwt>
{
"case_id": "c9e1...",
"subject": {
"full_name": "محمد بن علي",
"full_name_transliterations": ["Mohamed Ben Ali","Muhammad Ibn Ali"],
"dob": "1985-03-12",
"nationality": "TN"
},
"lists": ["OFAC","UN","EU","HMT","BCT_LOCAL"],
"include_pep": true,
"include_adverse_media": true
}
200 OK
{
"screening_id": "aa12...",
"hits_count": 1,
"decision": "manual_review",
"hits": [
{ "list_source":"OFAC", "match_score":0.86,
"match_type":"sanction", "record_id":"SDN-1234" }
]
}

6.2.3 Générer et soumettre une déclaration FATCA

Section intitulée « 6.2.3 Générer et soumettre une déclaration FATCA »
POST /v1/tcr/declarations
Content-Type: application/json
Authorization: Bearer <jwt>
{
"reporting_fi_giin": "00Z148.00027.ME.788",
"reporting_period": "2025",
"type": "FATCA1",
"filer_category": "FATCA601",
"accounts_filter": { "reporting_year": 2025 },
"auto_submit_ides": false
}
202 Accepted
{
"declaration_id": "d1a2...",
"status": "generating",
"estimated_completion": "2026-04-22T10:12:00Z"
}

Webhook asynchrone tcr.declaration.generated :

{
"event": "tcr.declaration.generated",
"timestamp": "2026-04-22T10:11:47Z",
"data": {
"declaration_id": "d1a2...",
"message_ref_id": "00Z148.00027.ME.788-20260422T101147-...",
"xml_download_url": "https://api.vitakyc.io/v1/tcr/declarations/d1a2.../xml",
"file_hash_sha256": "4f2a8...",
"record_count": 187
}
}

6.2.4 Consulter un bénéficiaire effectif via RNE

Section intitulée « 6.2.4 Consulter un bénéficiaire effectif via RNE »
POST /v1/kyb/rne/lookup
Content-Type: application/json
Authorization: Bearer <jwt>
{
"rne_identifier": "B0123456",
"services": ["kyc","ubo"]
}
200 OK
{
"kyc": {
"legal_name": "ACME TUNISIE SA",
"capital_amount": 500000.00,
"capital_currency": "TND",
"registration_date": "2012-06-14",
"status": "active",
"officers": [...]
},
"ubo": [
{ "individual_id": "P9876", "full_name": "...",
"ownership_pct": 42.5, "control_nature": "direct" }
],
"source": "rne.e-rne.tn",
"retrieved_at": "2026-04-22T09:57:21Z",
"cache_ttl_hours": 24
}

Fallback : si le service RNE est indisponible (timeout 5 s ou erreur 5xx), connector-rne bascule sur un état degraded ; kyc-svc passe le dossier en branche « capture manuelle d’extrait RNE + OCR ».

7.4 TCR FATCA — cycle FATCA 1 puis correction FATCA 3 + FATCA 1

Section intitulée « 7.4 TCR FATCA — cycle FATCA 1 puis correction FATCA 3 + FATCA 1 »

Format : JWT signé Ed25519 (pas de chiffrement, car le contenu est non sensible — c’est la signature qui compte).

{
"iss": "license-svc.vitakyc.io",
"sub": "tenant:<tenant_id>|installation:<installation_id>",
"iat": 1745309880,
"exp": 1776845880,
"aud": "vitakyc-runtime",
"modules": {
"kyc": { "features": ["kyc-p","kyb","nfc"], "limit_per_month": 50000 },
"aml": { "features": ["screening","ongoing"], "limit_per_month": 50000 },
"tcr": { "features": ["fatca","crs","templates-tn","templates-fr"],
"limit_per_year": 2 }
},
"deployment": "on_premise",
"regions": ["tn","fr"],
"support_tier": "enterprise",
"phone_home": false
}
  • SaaS : license-svc maintient la vérité en base ; vérification via appel RPC interne, cache 5 min.
  • On-prem : chaque service charge la clé publique Ed25519 (embarquée dans l’image), vérifie la signature localement. Re-vérification toutes les 24 h. Si phone_home activé, ping quotidien vers un endpoint VitaKYC pour rafraîchir la licence (révocation).
  • Dégradation : passée la date exp, grâce de 30 jours avec bannière dans la console. Au-delà, mode lecture seule.
  • Chaque module expose des feature flags évalués en temps réel : kyc.video-kyc, tcr.dac6, aml.transaction-monitoring.
  • Table feature_flag_override pour les activations ponctuelles (trials, beta) par tenant.
  • Flag library : Unleash (OSS) en SaaS, embarqué en on-prem.

  • Chaque service est une image OCI signée (cosign) publiée sur un registre privé VitaKYC.
  • Versioning SemVer (vitakyc/kyc-svc:1.12.3), release cadence bi-hebdo SaaS, trimestrielle on-prem.
  • SBOM (CycloneDX) embarqué dans chaque image.
  • Images distroless quand possible (UBI9-minimal sinon, pour compatibilité RHEL/OpenShift).

Un seul chart vitakyc-platform avec des values.yaml profiles :

charts/vitakyc-platform/
├── Chart.yaml
├── values.yaml # defaults
├── values-saas-multi-tenant.yaml
├── values-saas-dedicated.yaml
├── values-on-prem-small.yaml # jusqu'à 100 k checks/mois
├── values-on-prem-medium.yaml # jusqu'à 1 M checks/mois
├── values-on-prem-large.yaml
└── templates/
├── api-gateway/
├── identity-svc/
├── kyc-svc/
├── aml-svc/
├── tcr-svc/
├── ... (tous les services)
└── dependencies/ # sub-charts : postgres, kafka, redis, minio, keycloak, vault

Exemple d’extrait values-on-prem-small.yaml :

global:
deploymentMode: on_premise
licenseKeyPath: /var/run/secrets/vitakyc/license.jwt
airGap: false # true pour clients sans accès Internet
dataResidency: tn
defaultStorageClass: fast-ssd
postgresql:
enabled: true # déploiement embarqué (vs external: true pour Oracle existant du client)
replicaCount: 2
storage: 500Gi
kafka:
mode: redpanda # ou "kafka" pour vrai Kafka
replicaCount: 3
ocr:
inferenceMode: cpu # vs gpu
replicas: 2
resources:
limits: { cpu: "4", memory: "8Gi" }

SaaS multi-tenant (AWS Frankfurt par défaut) :

  • 3 AZ, cluster EKS.
  • RDS PostgreSQL Multi-AZ + read replica cross-region (DR).
  • MSK (Kafka managé) 3 brokers.
  • S3 avec chiffrement SSE-KMS.
  • ElastiCache Redis.
  • CloudFront + WAF.
  • Secrets via AWS KMS + HashiCorp Vault pour les secrets applicatifs.

On-premise banque tier 2 tunisienne (scénario type AO La Poste Tunisienne) :

  • Cluster Kubernetes (OpenShift préféré si déjà en place) 3 masters + 5-8 workers.
  • PostgreSQL managé par le client (Oracle supporté) en HA.
  • Kafka ou Redpanda 3 brokers sur nœuds dédiés.
  • MinIO 4 nœuds erasure-coded.
  • Keycloak fédéré à l’AD/LDAP existant.
  • Vault cluster Raft 3 nœuds.
  • Accès Internet sortant autorisé uniquement vers endpoints de listes AML (ou mode fichier signé en air-gap).

MenaceExemple concretMitigation
SpoofingAttaque sur le parcours client (faux selfie généré)Liveness niveau 2 + anti-deepfake V2 + signature device attestation
TamperingModification d’un dossier closTable audit WORM + hash chain ; RLS PostgreSQL ; approvals multi-rôles
RepudiationClient conteste avoir signé un W-8BENE-signature horodatée + hash fichier + journal d’audit ; preuve cryptographique
Information disclosureLeak données client cross-tenantRLS PostgreSQL par tenant_id ; clé de chiffrement par tenant ; tests automatisés de cloisonnement
Denial of serviceFlood endpoint OCRRate limiting par API key + par tenant ; isolation ressources par tenant ; shadow traffic détection
Elevation of privilegeAgent se donne rôle adminRBAC strict, séparation des devoirs, audit sur toute action de gestion
  • Chiffrement : TLS 1.3 partout, AES-256-GCM au repos, clés rotées tous les 90 jours via Vault Transit.
  • PII protection : colonnes sensibles chiffrées niveau application (envelope encryption).
  • Secrets : jamais en ConfigMap. Vault dynamic secrets pour DB (credentials TTL 1 h).
  • Access control : OIDC + MFA obligatoire pour users back-office. API keys avec scopes explicites.
  • Supply chain : images signées cosign, admission controller (Kyverno) rejetant les images non signées.
  • Vuln management : scan Trivy à chaque build, bloquant sur CVE critical.
  • Pen tests : externes annuels ; bug bounty en continu (YesWeHack après la V1).
  • Chaque événement audité est haché : event_hash = SHA-256(previous_hash || canonical_json(event)).
  • previous_hash = hash du précédent event du même tenant (chaîne).
  • Publication périodique (hebdo) du dernier hash de chaque tenant dans un journal immutable (blockchain publique optionnelle pour clients régulés exigeants) — roadmap V2.
  • Vérification offline : tout auditeur peut reconstituer la chaîne et détecter toute altération.

  • Traces : OpenTelemetry auto-instrumentation JVM + spans manuels sur les décisions métier.
  • Métriques : Prometheus ; conventions OTEL sémantique + métriques métier (kyc_case_decisions_total{tenant,decision}, fatca_declaration_generation_seconds).
  • Logs : JSON structuré, champ trace_id obligatoire, shipping Loki.
IndicateurSLOFenêtreAction si raté
Disponibilité API publique99,9 %30 jours glissantsIncident P1
Latence P95 POST /v1/kyc/cases< 600 ms7 joursIncident P2
Latence P95 OCR bout-en-bout< 3 s7 joursInvestigation
Taux de succès livraison webhooks (tentatives < 5)≥ 99,5 %7 joursAudit infra
Auto-approval rate (indicateur qualité, pas SLO)≥ 85 %30 joursItération modèle
  1. Santé plateforme — disponibilité, taux erreur HTTP 5xx, queue Kafka lag, RPS API gateway.
  2. Dossiers KYC — volumes, drop-off par étape, temps de décision P50/P95/P99.
  3. AML operations — alertes ouvertes, time-to-close par sévérité, SLA agent.
  4. TCR compliance — déclarations générées, soumissions IDES, erreurs ICMM.
  5. Cost per check — coût infra + IA / check décomposé par module et tenant.

TailleChecks / moisNœuds K8sRAM totaleStockageRemarques
S100 k3 masters + 3 workers64 Go / worker2 ToPostgreSQL embarqué ; OCR CPU
M1 M3 + 6128 Go / worker8 ToKafka 3 brokers ; OCR 2-4 GPU selon mix
L10 M3 + 12256 Go / worker40 ToDB externe client recommandée ; OCR 4-8 GPU
XL50 M+3 + 24Architecture à calibrer au projet
  • Auto-scaling horizontal (HPA) sur services stateless sur 2 signaux : CPU + files d’attente Kafka lag.
  • Services IA : KEDA sur longueur de file « inference jobs ».
  • PostgreSQL : lecture via read replicas, partitionnement par mois pour audit_event, tables tcr_* partitionnées par reporting_period.
  • Backpressure : si saturation, passage en mode 429 contrôlé plutôt que timeouts.

13. Exemple concret — générateur FATCA XML v2.0

Section intitulée « 13. Exemple concret — générateur FATCA XML v2.0 »

Produire un fichier XML FATCA v2.0 conforme au cahier des charges DGI Tunisie V1.0-2019 et au schéma FatcaXML_v2.0.xsd (IRS Publication 5124).

  • Entrée : declaration_id (UUID).
  • Étapes :
    1. Charger la déclaration, le reporting_fi et ses account_report.
    2. Construire un MessageSpec (unique dans l’espace/temps).
    3. Construire le bloc FATCA (ReportingFI + ReportingGroup).
    4. Sérialiser via JAXB avec validation XSD stricte.
    5. Appliquer les échappements &amp;, &lt;, &gt;, &apos;, &quot; (JAXB par défaut OK).
    6. Générer le fichier <MF>-<exercice>.xml en UTF-8 sans BOM.
    7. Compresser GZIP si > seuil.
    8. Calculer SHA-256, stocker dans tcr_declaration.file_hash_sha256.
    9. Publier l’URL signée MinIO, évènement Kafka tcr.declaration.generated.

13.3 Extrait Kotlin (illustratif — pas de production)

Section intitulée « 13.3 Extrait Kotlin (illustratif — pas de production) »
class FatcaDeclarationBuilder(
private val crypto: HashService,
private val clock: Clock,
) {
fun build(ctx: FatcaBuildContext): FatcaDeclarationArtifact {
val now = clock.instant()
val messageRefId = buildMessageRefId(ctx.reportingFI.giin, now)
val messageSpec = MessageSpecType().apply {
sendingCompanyIN = ctx.reportingFI.giin
transmittingCountry = "TN"
receivingCountry = "US"
messageType = "FATCA"
messageRefID = messageRefId
reportingPeriod = "${ctx.reportingPeriod}-12-31"
timestamp = now
if (ctx.type != FatcaType.FATCA1) {
corrMessageRefID = ctx.parentMessageRefId
}
}
val reportingFi = CorrectableReportOrganisationType().apply {
resCountryCode = listOf("TN")
tin = TINType(ctx.reportingFI.giin, "US") // GIIN émis par l'IRS
name = listOf(NameOrganisationType(ctx.reportingFI.legalName))
address = buildAddress(ctx.reportingFI)
filerCategory = ctx.filerCategory.code // ex: "FATCA601"
docSpec = buildDocSpec(ctx.reportingFI.giin, ctx.type, ctx.parentDocRefId)
}
val reportingGroup = ReportingGroup().apply {
when (ctx.type) {
FatcaType.NIL -> nilReport = buildNilReport(ctx)
else -> accountReports.addAll(ctx.accounts.map { buildAccount(it, ctx) })
}
}
val fatca = FatcaType().apply {
this.reportingFI = reportingFi
this.reportingGroups.add(reportingGroup)
}
val root = FatcaOecd().apply {
this.version = "2.0"
this.messageSpec = messageSpec
this.fatca.add(fatca)
}
val xmlBytes = marshallWithStrictSchemaValidation(root)
val fileName = "${ctx.reportingFI.matriculeFiscal}-${ctx.reportingPeriod}.xml"
val hash = crypto.sha256(xmlBytes)
return FatcaDeclarationArtifact(xmlBytes, fileName, hash, messageRefId)
}
/**
* DGI Tunisie: <GIIN>-<ISO8601 UTC à la seconde>-<GUID>
* Garantit l'unicité dans l'espace et le temps, conformément à RG2.2.
*/
private fun buildMessageRefId(giin: String, now: Instant): String {
val ts = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss").format(now.atZone(UTC))
val guid = UUID.randomUUID().toString().replace("-", "")
return "$giin-$ts-$guid"
}
/**
* DocRefID : <GIIN>.<numéro unique>. Min 21 max 200 chars, [A-Za-z0-9.-] uniquement.
* Chaque ReportingFI / AccountReport / NilReport doit avoir un DocRefID distinct.
*/
private fun buildDocSpec(giin: String, type: FatcaType, parentDocRef: String?): DocSpec {
val docRefId = "$giin.${UUID.randomUUID().toString().replace("-", "")}"
return DocSpec(
docTypeIndic = type.indicativeCode, // "FATCA1", "FATCA2", ...
docRefID = docRefId,
corrMessageRefID = parentDocRef?.let { /* parent message */ },
corrDocRefID = parentDocRef
)
}
}

Fichier : 0005623A-2025.xml — UTF-8 sans BOM.

<?xml version="1.0" encoding="UTF-8"?>
<ftc:FATCA_OECD version="2.0"
xmlns:ftc="urn:oecd:ties:fatca:v2"
xmlns:sfa="urn:oecd:ties:stffatcatypes:v2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:oecd:ties:fatca:v2 FatcaXML_v2.0.xsd">
<sfa:MessageSpec>
<sfa:SendingCompanyIN>00Z148.00027.ME.788</sfa:SendingCompanyIN>
<sfa:TransmittingCountry>TN</sfa:TransmittingCountry>
<sfa:ReceivingCountry>US</sfa:ReceivingCountry>
<sfa:MessageType>FATCA</sfa:MessageType>
<sfa:MessageRefId>00Z148.00027.ME.788-20260422T101147-406abc8a1830490e847890ba3b13a646</sfa:MessageRefId>
<sfa:ReportingPeriod>2025-12-31</sfa:ReportingPeriod>
<sfa:Timestamp>2026-04-22T10:11:47Z</sfa:Timestamp>
</sfa:MessageSpec>
<ftc:FATCA>
<ftc:ReportingFI>
<sfa:ResCountryCode>TN</sfa:ResCountryCode>
<sfa:TIN issuedBy="US">00Z148.00027.ME.788</sfa:TIN>
<sfa:Name>BANQUE EXEMPLE DE TUNISIE</sfa:Name>
<sfa:Address>
<sfa:CountryCode>TN</sfa:CountryCode>
<sfa:AddressFix>
<sfa:Street>Avenue Habib Bourguiba</sfa:Street>
<sfa:BuildingIdentifier>25</sfa:BuildingIdentifier>
<sfa:PostCode>1000</sfa:PostCode>
<sfa:City>Tunis</sfa:City>
</sfa:AddressFix>
</sfa:Address>
<ftc:FilerCategory>FATCA601</ftc:FilerCategory>
<ftc:DocSpec>
<sfa:DocTypeIndic>FATCA1</sfa:DocTypeIndic>
<sfa:DocRefId>00Z148.00027.ME.788.406abc8a1830490e847890ba3b13a646</sfa:DocRefId>
</ftc:DocSpec>
</ftc:ReportingFI>
<ftc:ReportingGroup>
<ftc:AccountReport>
<ftc:DocSpec>
<sfa:DocTypeIndic>FATCA1</sfa:DocTypeIndic>
<sfa:DocRefId>00Z148.00027.ME.788.b2e6a3d9c4e14bfe9b3b9c14f58b0b11</sfa:DocRefId>
</ftc:DocSpec>
<ftc:AccountNumber AcctNumberType="OECD601">TN59 1000 1234 5678 9012 3456</ftc:AccountNumber>
<ftc:AccountHolder>
<ftc:Individual>
<sfa:ResCountryCode>US</sfa:ResCountryCode>
<sfa:TIN issuedBy="US">123-45-6789</sfa:TIN>
<sfa:Name>
<sfa:FirstName>JOHN</sfa:FirstName>
<sfa:LastName>SMITH</sfa:LastName>
</sfa:Name>
<sfa:Address>
<sfa:CountryCode>TN</sfa:CountryCode>
<sfa:AddressFix>
<sfa:Street>Rue de la Liberte</sfa:Street>
<sfa:PostCode>2045</sfa:PostCode>
<sfa:City>La Marsa</sfa:City>
</sfa:AddressFix>
</sfa:Address>
<sfa:BirthInfo>
<sfa:BirthDate>1980-06-15</sfa:BirthDate>
<sfa:CountryInfo>
<sfa:CountryCode>US</sfa:CountryCode>
</sfa:CountryInfo>
</sfa:BirthInfo>
</ftc:Individual>
</ftc:AccountHolder>
<ftc:AccountBalance currCode="USD">125430.75</ftc:AccountBalance>
</ftc:AccountReport>
</ftc:ReportingGroup>
</ftc:FATCA>
</ftc:FATCA_OECD>
  • Encodage UTF-8 sans BOM vérifié
  • Taille < 100 Mo avant compression
  • Nommage <MF>-<exercice>.xml respecté (caractères autorisés uniquement)
  • Aucune séquence --, /*, &# dans le contenu
  • MessageRefId unique (GIIN + timestamp seconde + GUID)
  • DocRefId unique par ReportingFI, AccountReport, NilReport (21–200 chars, format <GIIN>.<id>)
  • FilerCategory valide (FATCA601 pour IF étrangère participante par défaut)
  • TIN titulaire : neuf A si inconnu + personne physique ; neuf 0 si EENF passive
  • Chaînage CorrMessageRefId / CorrDocRefId renseigné pour FATCA 2 / 3 / 4
  • Validation XSD locale avant dépôt (schéma FatcaXML_v2.0.xsd)
  • Hash SHA-256 calculé et stocké
  • Pour FATCA 3+1 : déposer FATCA 1 uniquement après la fenêtre DGI → IRS de FATCA 3

  • Unitaire (60 %) : JUnit 5 + AssertJ + MockK (Kotlin) ; pytest pour services Python.
  • Contrat (15 %) : Pact ou Spring Cloud Contract entre services critiques.
  • Intégration (15 %) : Testcontainers (PostgreSQL, Kafka, Redis, MinIO réels).
  • End-to-end (8 %) : Playwright pour parcours UI + k6 pour scénarios API.
  • Performance (2 %) : k6 scénarios cibles sur staging (cf. SLOs).
  • Corpus d’échantillons XML fournis par l’IRS (fichiers de tests Publication 5124).
  • Tests de non-régression XSD sur chaque release.
  • Test de chaînage : matrice 4×4 (type courant × type parent).
  • Test air-gap : génération + compression + dépôt manuel simulé sans réseau.
  • Script automatisé vérifiant qu’un tenant A ne peut jamais voir les données d’un tenant B (via API, via SQL direct avec mauvais contexte, via Kafka).
  • Test exécuté à chaque PR touchant identity-svc ou les politiques RLS.

15.1 Correspondance Cahier des charges → Architecture

Section intitulée « 15.1 Correspondance Cahier des charges → Architecture »
CdCDocument technique
§4.1 KYC individuel§4 services kyc-svc, ocr-svc, liveness-svc, face-match-svc ; §7.1 parcours
§4.2 AML§4 service aml-svc ; §7.2 ongoing monitoring
§4.3 TCR§4 service tcr-svc, report-svc ; §7.4 cycle correction ; §13 exemple concret
§5 Licensing modulaire§8 clé Ed25519 + feature flags
§6 Stack technique§4.4 choix d’infra ; §9 déploiement
§7 Déploiement hybride§9 Helm chart unifié + topologies
§10 Sécurité§10 threat model + contrôles ; §5 chiffrement par tenant
§12 Intégrations§4 connectors ; §7.3 parcours RNE ; §13 FATCA IDES

15.2 ADR à produire en priorité par l’équipe tech

Section intitulée « 15.2 ADR à produire en priorité par l’équipe tech »
  1. ADR-001 — Choix Temporal vs Camunda 8 pour workflows.
  2. ADR-002 — Stratégie multi-tenant (shared schema + RLS) vs schema-per-tenant.
  3. ADR-003 — Moteur OCR : stack build interne vs fallback commercial.
  4. ADR-004 — Choix du framework de règles métier (OPA ? DMN ? règles Kotlin ?).
  5. ADR-005 — Stockage des documents : MinIO vs abstraction cloud-native.
  6. ADR-006 — Stratégie de gestion des listes AML (formats, refresh, air-gap).
  • VitaKYC_Cahier_des_Charges.md
  • VitaKYC_Etude_Concurrentielle.md
  • CC- AO N12-UCA-DAI-2025 E-KYC- V final.pdf — AO La Poste Tunisienne
  • Cahier_des_charges-1.pdf — Cahier des charges DGI Tunisie FATCA V1.0-2019 (schéma V3.7)
  • IRS Publication 5124 — FATCA XML Schema v2.0 User Guide
  • IRS Publication 5189 — ICMM Notification User Guide
  • IRS Form 8966 XML Schema FatcaXML_v2.0.xsd + types isofatcatypes_v1.1.xsd, oecdtypes_v4.2.xsd, stffatcatypes_v2.0.xsd

Fin du Dossier d’Architecture Technique.