Aller au contenu

Décisions d'architecture (ADRs)

Version : 2.2 Date : 1er mai 2026 Auteur : Chaouki Barkia

Ce document regroupe les 25 décisions d’architecture validées au démarrage et étendues progressivement (mobile S+24, Risk Matrix S+25, intégration CPS S+26, moteur Form Designer S+27, pipeline biométrique S+28, case management S+29, sanctions screening S+30, transaction monitoring streaming S+31, webhooks signés S+32, auth/authz S+33, TCR FATCA/CRS S+34, observability S+35, audit log centralisé S+36, convention JSON snake_case S+37). Chaque ADR suit un format standard (statut, contexte, décision, conséquences, alternatives). Toute modification ultérieure doit faire l’objet d’un nouvel ADR daté, conservant la traçabilité.



Statut : Proposé — à valider par l’équipe technique au kickoff Date : 2026-04-22 Décideurs : Tech Lead, Architecte

La plateforme VitaKYC orchestre des workflows longs, durables, multi-étapes, avec retry et interventions humaines :

  • Un dossier KYC peut rester ouvert plusieurs jours (capture document, attente de revue agent, suites éventuelles).
  • Le screening AML ongoing tourne en continu pendant toute la relation client.
  • La revalidation FATCA (formulaires W-8) a un horizon de 3 ans.
  • Les corrections FATCA 3+1 doivent attendre une fenêtre DGI → IRS avant d’envoyer la réémission (contrainte temporelle stricte).

Un moteur maison serait long à durcir (retry, idempotence, historique, signals). Les candidats sérieux sont Temporal.io, Camunda 8 (Zeebe), et dans une moindre mesure AWS Step Functions.

Adopter Temporal.io (self-hosted) comme moteur de workflow pour tous les traitements long-running.

  • Modèle de programmation : on écrit des workflows en Kotlin/Java comme du code impératif avec garanties de durabilité (reprise après crash, retry automatique, signals). C’est plus proche du code métier que BPMN.
  • Fiabilité : Temporal est éprouvé chez Uber, Stripe, Coinbase ; activités idempotentes natives, exactly-once execution.
  • Support long-running : sommeils de plusieurs mois, voire années, gérés nativement (workflow.sleep(Duration.ofDays(365*3)) — idéal pour revalidation W-8).
  • Observabilité : UI Temporal = debug puissant (history d’un workflow, état actuel, replay).
  • Communauté et écosystème : Kotlin SDK mature, métriques Prometheus natives, déploiement Kubernetes documenté.
  • Licence : MIT — utilisable on-prem sans surcoût.

Positives

  • Réduction du code de plomberie (retry, état, persistance) de ~30-40 %.
  • Chaînage FATCA 1/3 facile à implémenter comme un workflow : fatca3 puis workflow.sleep(untilDgiIrsWindowClosed) puis fatca1.
  • Tests reproductibles (replay d’historique).

Négatives

  • Courbe d’apprentissage : les ingénieurs doivent comprendre les determinism constraints (code workflow ≠ code activity).
  • Infrastructure additionnelle : cluster Temporal (historyservice + matchingservice + frontend + workers) à opérer en on-prem, ~3 pods minimum.
  • Dépendance PostgreSQL ou Cassandra pour le store Temporal.

Risques & mitigations

  • Risque : verrouillage autour de Temporal. Mitigation : garder la logique métier dans des services découplés, les workflows étant des orchestrateurs minces. On peut porter vers Camunda 8 au prix d’un refactoring contenu.
  • Risque : montée en charge imprévue de l’historique Temporal. Mitigation : retention configurable par namespace, archivage S3.
OptionPourquoi non retenue
Camunda 8 (Zeebe)Excellent produit, mais modèle BPMN moins fluide pour du code métier riche ; licence freemium avec limites de volume.
AWS Step FunctionsCloud-locked, impossible en on-prem — contraire à la promesse hybride de VitaKYC.
Moteur interne (ad hoc)Risque élevé de réinventer la roue imparfaitement ; 6-12 mois perdus avant production robuste.
Airflow / DagsterOrientés data pipelines, pas orchestration métier long-running avec signals.
  • temporal.io
  • VitaKYC Architecture Technique §4.4, §7.4

ADR-002 — Multi-tenant : shared schema + Row Level Security PostgreSQL

Section intitulée « ADR-002 — Multi-tenant : shared schema + Row Level Security PostgreSQL »

Statut : Proposé Date : 2026-04-22

VitaKYC doit supporter trois modes de déploiement :

  1. SaaS multi-tenant partagé — plusieurs clients sur la même infrastructure.
  2. SaaS dédié — infra dédiée par client (enterprise).
  3. On-premise — single-tenant par construction.

La décision porte surtout sur le mode 1 : comment isoler les données de plusieurs tenants sur la même base de données tout en permettant l’exploitation opérationnelle (backups, migrations, analytics cross-tenant) ?

Les trois schémas d’isolation courants sont :

  • Database per tenant — isolation maximale, coût opérationnel exponentiel.
  • Schema per tenant — bon compromis mais multiplication des objets, migrations complexes.
  • Shared schema avec discrimination par tenant_id — simple à opérer, risque de fuite si erreur applicative.

Adopter shared schema avec tenant_id sur chaque table métier, renforcé par Row Level Security (RLS) PostgreSQL activée par défaut, et chiffrement des données sensibles avec clé par tenant (HashiCorp Vault Transit).

  • Opérabilité : une seule base, une seule migration, une seule stratégie de backup. Critique pour une équipe lean au démarrage.
  • RLS PostgreSQL : garantit en profondeur que même une requête SQL mal formée ne peut pas fuir entre tenants (ceinture + bretelles vs sécurité applicative seule).
  • Chiffrement par tenant : un compromis d’une base ne révèle pas les PII en clair ; rotation possible par tenant.
  • Coût : ~10× moins cher que database-per-tenant à volume équivalent.
  • Dégradation simple vers SaaS dédié et on-prem : même code, infrastructure différente.
-- Chaque table métier porte tenant_id
CREATE TABLE kyc_case (
case_id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
-- ...autres colonnes
);
-- RLS activée
ALTER TABLE kyc_case ENABLE ROW LEVEL SECURITY;
ALTER TABLE kyc_case FORCE ROW LEVEL SECURITY;
-- Policy : le tenant est récupéré d'une variable de session positionnée
-- par l'application à l'ouverture de connexion
CREATE POLICY tenant_isolation ON kyc_case
USING (tenant_id = current_setting('app.current_tenant')::uuid);
-- L'app fait :
-- SET app.current_tenant = '<uuid>';

Positives

  • Simplicité opérationnelle maximale.
  • Possibilité d’analytics cross-tenant (anonymisés) pour l’équipe VitaKYC (benchmarks, product analytics).

Négatives

  • Obligation absolue de positionner la session app.current_tenant avant toute requête — middleware dédié requis dans Spring Boot (filtre JPA).
  • Test automatisé obligatoire à chaque release qui vérifie qu’un tenant ne voit pas les données d’un autre (§14.3 du doc archi).
  • PostgreSQL-bound (RLS n’existe pas de la même façon en MySQL) — acceptable vu que PostgreSQL est le choix principal.
OptionPourquoi non retenue
Database per tenantCoût opérationnel trop élevé au MVP ; retardera le time-to-market.
Schema per tenantMigrations multipliées par N tenants ; complexité inutile vu que RLS donne 95 % du bénéfice.
Isolation uniquement applicative (sans RLS)Insuffisant pour un produit compliance : une seule requête WHERE oubliée = fuite.

Le plan Regulated On-Prem et le plan SaaS dédié déploient une base par tenant — même code, topologie différente. Le tenant_id reste présent mais contient une seule valeur.


ADR-003 — OCR : build hybride (moteur interne + fallback commercial)

Section intitulée « ADR-003 — OCR : build hybride (moteur interne + fallback commercial) »

Statut : Proposé Date : 2026-04-22

L’OCR est central pour le KYC (passeports, CNI, justificatifs domicile, extraits RNE). Les contraintes sont :

  • Multilinguisme critique : français, anglais, arabe (impératif pour MENA), chiffres hindi (documents égyptiens).
  • Précision : ≥ 98 % sur documents nominaux sinon drop-off client.
  • Coût unitaire : OCR représente le poste 1 du coût par vérification (~0,05 à 0,50 $ selon fournisseur).
  • Portabilité on-prem : certains clients interdisent l’appel à un SaaS externe.

Options : 100 % fournisseur commercial (Google Vision, AWS Textract, Mistral OCR, Veryfi, Regula), 100 % moteur interne (Tesseract + modèles custom ONNX), ou hybride.

Stratégie hybride avec routage :

  1. Moteur interne par défaut : Tesseract 5 + modèles de détection de champ fine-tunés sur documents MENA (pipeline PyTorch → export ONNX) ; déployé dans ocr-svc.
  2. Fallback commercial configurable par tenant : si le score de confiance du moteur interne < seuil, bascule sur un fournisseur commercial (ordre de préférence configurable).
  3. Mode air-gap : en on-prem sans accès Internet, seul le moteur interne est actif — les clients sont informés du trade-off (précision un peu inférieure sur documents rares).
  • Coût marginal maîtrisé : sur le gros des documents courants (CNI TN/FR/MA, passeports ICAO), le moteur interne suffit → 0 coût marginal externe.
  • Qualité sur le long tail : les fournisseurs commerciaux gardent l’avantage sur documents rares ou dégradés ; le fallback permet de ne pas rejeter inutilement.
  • Flexibilité commerciale : les clients enterprise peuvent négocier avec leur propre fournisseur data et le brancher ; VitaKYC reste neutre.
  • Roadmap d’internalisation : au fur et à mesure que le dataset d’entraînement grandit, le moteur interne dépasse le fallback et le fallback devient exceptionnel.
┌────────────┐
doc ────▶│ ocr-svc │
│ │
│ 1. route │
└─────┬──────┘
┌───────┴────────┐
▼ ▼
┌───────────┐ ┌──────────────┐
│ moteur │ │ adapter │
│ interne │ │ commercial │
│ (ONNX) │ │ (Google, │
│ │ │ Regula, …) │
└────┬──────┘ └──────┬───────┘
│ score < seuil │
└────────┬─────────┘
┌────────────┐
│ résultat │
│ consolidé │
└────────────┘

Positives

  • Coût marginal réduit de 40-60 % vs 100 % commercial.
  • Compatibilité air-gap.
  • Négociation commerciale facilitée (pas de surcoût visible pour le client).

Négatives

  • Deux systèmes à maintenir (interne + adapters commerciaux).
  • Nécessite un data engineer / ML engineer dès M+6 pour entraîner et mesurer.
  • Courbe d’apprentissage sur MLOps (monitoring de drift, retrain).
  • 100 % commercial : simple mais coût marginal élevé et incompatible air-gap.
  • 100 % interne : impossible à la précision requise au MVP sans dataset massif.
  • Build custom à partir de zéro (CNN + CTC maison) : hors budget et hors délai au MVP.
  • Moteur interne : précision character-level ≥ 97 % sur passeports FR/TN/EG au MVP ; ≥ 99 % à V2.
  • Taux de fallback vers commercial ≤ 15 % au MVP, ≤ 5 % à V2.

ADR-004 — Moteur de règles métier : DSL Kotlin + OPA pour politiques

Section intitulée « ADR-004 — Moteur de règles métier : DSL Kotlin + OPA pour politiques »

Statut : Proposé Date : 2026-04-22

VitaKYC a deux natures de règles à gérer :

  1. Règles métier riches — décisions KYC, détection d’indicia FATCA, calcul de risque AML composite. Elles changent souvent, dépendent de données métier, et doivent être versionnées avec des tests unitaires.
  2. Politiques d’accès / compliance — qui peut voir quoi, quel tenant peut exporter vers quelle résidence de données. Elles changent rarement mais doivent être auditables.

Options évaluées : Drools, OPA (Open Policy Agent), DMN via Camunda, DSL Kotlin maison, règles en base avec éditeur UI.

Approche double :

  • Règles métierDSL Kotlin (type-safe, versionné dans le monorepo, testé) + interface admin pour les paramètres numériques et listes (seuils, pondérations, listes de pays) stockés en base.
  • Politiques d’accès / complianceOpen Policy Agent (OPA) avec policies Rego, déployé en sidecar opa sur les services.

Pour le DSL Kotlin :

  • Réutilisation immédiate des compétences équipe.
  • Tests unitaires triviaux (JUnit + Kotest).
  • Pas de dépendance runtime supplémentaire.
  • Refactoring sûr via IDE.

Pour OPA :

  • Standard de facto pour policy-as-code en Kubernetes / microservices.
  • Découplage fort : les politiques peuvent être modifiées sans redéploiement de service.
  • Bundles signés pour distribution sécurisée des policies (air-gap friendly).
object KycDecisionRules {
fun evaluate(ctx: KycContext): Decision {
val score = ctx.aggregateRiskScore()
return when {
score >= ctx.tenantConfig.autoApproveThreshold ->
Decision.AutoApprove(score)
score >= ctx.tenantConfig.manualReviewThreshold ->
Decision.ManualReview(score, reasons = ctx.flaggedReasons)
ctx.fatcaIndiciaCount >= 2 && ctx.usPersonSelfCertified.not() ->
Decision.ManualReview(score, reasons = listOf("FATCA_INDICIA_UNCONFIRMED"))
ctx.amlHitsHigh.isNotEmpty() ->
Decision.Escalate(score, reasons = ctx.amlHitsHigh.map { it.key })
else ->
Decision.Reject(score, reasons = listOf("INSUFFICIENT_VERIFICATION"))
}
}
}
package vitakyc.export
# Un agent ne peut exporter les données d'un dossier que si :
# - il appartient au tenant du dossier
# - il a le rôle supervisor ou admin
# - la résidence des données du tenant autorise la région cible
allow {
input.user.tenant_id == input.resource.tenant_id
input.user.roles[_] == role
role == "supervisor"
data.tenants[input.user.tenant_id].residency_regions[_] == input.export.target_region
}

Positives

  • Règles métier : versionnées, testées, rapide à écrire.
  • Politiques : modifiables indépendamment du code, auditables, cross-language.

Négatives

  • Les équipes doivent apprendre Rego (OPA) — courbe courte mais réelle.
  • Éviter la tentation de tout migrer vers OPA — garder la séparation nette.
  • Drools : puissant mais lourd, syntaxe DRL vieillissante, outillage daté.
  • DMN via Camunda : bon pour décisions tabulaires simples, surdimensionné pour règles riches.
  • Règles en base avec éditeur UI : attrayant mais typage perdu, testabilité faible ; reporté à V2 comme extension OPA + UI low-code.

ADR-005 — Stockage des documents : MinIO / S3 compatible + couche d’abstraction

Section intitulée « ADR-005 — Stockage des documents : MinIO / S3 compatible + couche d’abstraction »

Statut : Proposé Date : 2026-04-22

Chaque dossier KYC génère des documents : scans recto/verso, selfies, vidéos VideoKYC, PDFs signés W-8/W-9, fichiers XML FATCA, exports de rapports. Le stockage doit être :

  • Chiffré au repos (AES-256) avec clé par tenant.
  • Immutable après finalisation (legal hold, rétention 10 ans).
  • Portable (SaaS AWS/GCP, on-prem datacenter).
  • Signable pour URLs temporaires (téléchargement client / agent).

API S3 comme contrat unique :

  • SaaS : AWS S3 (Frankfurt) ou GCS selon région.
  • On-prem : MinIO déployé dans le cluster Kubernetes (4 nœuds minimum en erasure coding).
  • Un service dédié doc-store encapsule l’accès : le code métier n’appelle jamais directement l’API S3.
  • API S3 = standard universel : SDK Kotlin/Java (AWS SDK v2), Python (boto3) — fonctionne identiquement sur AWS, GCS (via interopérabilité), MinIO.
  • MinIO : compatibilité S3 à 99 %, performance élevée, chiffrement SSE-KMS natif intégrable avec Vault.
  • Couche d’abstraction doc-store : permet d’ajouter des politiques transverses (legal hold, retention, anonymisation, watermarking) sans disperser le code.
Pattern : vitakyc-{env}-{region}-{purpose}
Exemples :
vitakyc-prod-eu-docs # documents clients chiffrés
vitakyc-prod-eu-exports # exports générés (FATCA XML, rapports)
vitakyc-prod-eu-audit-archive # archives immutables (WORM)
Préfixe objet : {tenant_id}/{kind}/{YYYY}/{MM}/{case_id}/{file_id}
Exemple :
c9e1.../kyc-documents/2026/04/abc123.../passport-recto.jpg.enc

Positives

  • Portabilité totale SaaS ↔ on-prem.
  • Un seul SDK à maintenir.
  • Audit unifié des accès documents.

Négatives

  • Coût MinIO on-prem : opération cluster 4-node minimum pour HA.
  • Surcharge CPU pour chiffrement client-side quand imposé.
  • NFS / filesystem : pas de chiffrement granulaire, scalabilité limitée.
  • Azure Blob : API différente, forcerait du code conditionnel.
  • Ceph Object Gateway : puissant mais opérer Ceph est un métier à part ; MinIO est plus léger.

ADR-006 — Gestion des listes AML : pipeline incrémental avec support air-gap

Section intitulée « ADR-006 — Gestion des listes AML : pipeline incrémental avec support air-gap »

Statut : Proposé Date : 2026-04-22

Le screening AML dépend de listes mises à jour en permanence :

  • Sanctions : OFAC SDN, OFAC Consolidated, UN 1267, EU consolidated, HMT UK, listes nationales (BCT Tunisie, SEPBLAC, CB UAE).
  • PEP : fournisseurs tiers (Dow Jones, LSEG World-Check, ComplyAdvantage data).
  • Adverse media : flux NLP continu.

En mode SaaS, on peut poller régulièrement les flux officiels et les providers. En mode on-prem air-gap, les clients interdisent les connexions sortantes — les listes doivent être distribuées sous forme de fichiers signés.

Pipeline en quatre étapes centralisé côté VitaKYC, puis distribution :

  1. Ingestion : poller les sources officielles (OFAC, UN, EU…) et les APIs des providers ; normaliser vers un format commun VKL v1 (VitaKYC Lists).
  2. Normalisation : canonicalisation des noms (arabe → latin + unicode normalization), extraction des identifiants (dates de naissance, nationalité, AKA), scoring de confiance.
  3. Publication : trois canaux
    • SaaS : mise à jour directe dans OpenSearch (index miroir par source).
    • On-prem avec Internet : pull quotidien depuis un endpoint sécurisé lists.vitakyc.io (bundle signé).
    • On-prem air-gap : export mensuel ou hebdomadaire sur support physique (USB chiffré) ou transfert fichier sFTP one-way, avec vérification de signature côté client.
  4. Consommation : aml-svc interroge OpenSearch ; le pipeline de matching est identique quels que soient le mode de distribution.
{
"vkl_version": 1,
"source": "OFAC_SDN",
"snapshot_id": "ofac-sdn-2026-04-22T06:00:00Z",
"published_at": "2026-04-22T06:15:00Z",
"signature": "ed25519:MEUCIQ...",
"records": [
{
"id": "SDN-12345",
"type": "individual",
"names": ["John Doe","ДЖОН ДОУ","جون دو"],
"dob": ["1965-03-12"],
"nationalities": ["IR"],
"aka": ["Johnny Doe"],
"addresses": [{"country":"IR","city":"Tehran"}],
"programs": ["IRAN"],
"sanctions_since": "2015-06-10",
"last_modified": "2024-11-08"
}
]
}

Positives

  • Matching identique entre modes de déploiement.
  • Les clients air-gap ne sont pas désavantagés fonctionnellement, juste en fraîcheur.
  • Les providers commerciaux peuvent être substitués sans toucher au code métier.

Négatives

  • Responsabilité éditoriale VitaKYC : maintenir le pipeline, monitorer la qualité des sources.
  • Risque juridique : toute erreur de liste peut avoir des conséquences pour les clients. → Clause contractuelle de best-effort + mention de responsabilité du client sur la décision finale.
  • Sourcing initial des données PEP : build interne impossible au MVP (coût d’un dataset mondial ~200-500 k€/an) → recommandation : partenariat avec ComplyAdvantage ou Dow Jones en revente au lancement, build progressif ensuite.
  • Fréquence d’actualisation air-gap : négociée contractuellement (hebdomadaire = standard, quotidien = premium).

  • Tout ADR doit être revu par le tech lead + 1 ingénieur senior minimum avant passage au statut Accepté.
  • Un ADR Accepté peut être Superseded par un nouvel ADR (nouveau numéro) qui référence explicitement l’ancien.
  • Aucun ADR ne doit être supprimé — la traçabilité des décisions est un livrable produit.

ADR-007 — Form Designer : scope MVP (no-code, configurable par tenant)

Section intitulée « ADR-007 — Form Designer : scope MVP (no-code, configurable par tenant) »

Statut : Accepté Date : 2026-04-22

Au premier cadrage, nous avions proposé de reporter le Form Designer (éditeur no-code de parcours KYC) à la V1, en s’appuyant sur des templates YAML pré-livrés. Retour terrain du marché tunisien :

« En Tunisie, on ne peut pas vendre si tu n’as pas le form designer. »

Les raisons observées :

  • Les AO publics et cahiers des charges bancaires tunisiens expriment explicitement une exigence de « paramétrage » et de « personnalisation » des parcours.
  • Les banques locales disposent de multiples produits (compte courant, compte jeune, compte pro, crédit, cartes) avec des formulaires distincts qu’elles veulent répliquer exactement.
  • Les concurrents régionaux (uqudo, Valify) affichent des parcours configurables comme fonctionnalité standard.
  • Une position « parcours standards + YAML backend » est perçue comme un manque de maturité produit et ferme la porte commerciale.

Le Form Designer est inclus dans le périmètre MVP (V0) avec le périmètre fonctionnel décrit au §4.1.0 du cahier des charges : drag-drop de champs, règles de visibilité, localisation FR/AR/EN, versioning par tenant, templates sectoriels, preview live, rendu dynamique via le SDK Web.

  • Gate commercial : sans Form Designer, pas de deal MENA au M+6. Le MVP perd sa raison d’être.
  • Différenciation : renforce la promesse « hybride + modulaire + MENA-first » avec un attribut produit concret.
  • Effet boule de neige : le Form Designer crée la surface d’intégration pour les futurs modules (AML screening inline, TCR indicia capture), donc le construire tôt évite de refactorer plus tard.

Positives

  • Déblocage commercial du marché tunisien et plus largement MENA.
  • Capacité à répondre aux AO publics avec un produit “paramétrable” par l’acheteur.
  • Architecture plus extensible dès le MVP.

Négatives

  • + 67 SP au backlog MVP (épic E17) — dépassement de ~15 % de la capacité initiale des 12 sprints.
  • Nécessite un frontend senior de plus (ou extension du MVP à 14 sprints).
  • Complexité DB accrue (JSONB pour définitions + données collectées), plus de tests de non-régression.
  • Pas d’A/B testing au MVP — reporté à V1.
  • Pas de logique avancée (boucles, sous-formulaires dynamiques) au MVP — règles simples seulement.
  • Pas d’intégration Webhook/API sur événement de champ au MVP.
  • Mapping CPS obligatoire sur chaque champ (amendement 2026-04-23, cf. ADR-026) — chaque champ doit déclarer à quelle variable du Client Profile Schema il alimente (ex : client.profession, client.country, client.residentStatus). Les champs non-mappés sont valides (ex : consentement RGPD, notes internes) mais explicitement flaggés “internal_only” pour éviter qu’ils soient confondus avec des variables utilisables par le Risk Matrix.
  • Refus de suppression / renommage (amendement 2026-04-23, cf. ADR-026) — un champ référencé par au moins une policy de risque ACTIVE ou SHADOW ne peut pas être supprimé. La suppression est proposée après deprecated=true pendant une fenêtre de 90 jours, OU après retrait manuel de toutes les règles côté compliance.
  • Événement form.published (amendement 2026-04-23, cf. ADR-026) — chaque publication émet un event Kafka consommé par profile-schema-svc pour tenir à jour le CPS. Payload : {tenantId, formId, version, addedFields[], removedFields[], renamedFields[]}. Compliance est notifié des changements structurants.
  • À confirmer en démo fin S08 : le Form Designer doit être montrable à un prospect en direct.
  • En cas de dérapage de charge > 20 %, revue Architecture + Product Management avant extension ou réduction de scope.
OptionPourquoi écartée
YAML templates + code-only customizationRejetée par le marché tunisien ; perçue comme non mature.
Form Designer reporté V1 avec “promesse roadmap”Ne gagne pas les AO ; perd 6 mois de revenu.
OEM ou revente d’un form designer tiers (Formspree / Form.io)Intégration profonde requise avec les briques KYC (OCR, liveness, signature) — coût d’intégration supérieur au build ciblé.
  • VitaKYC_Cahier_des_Charges.md §4.1.0
  • VitaKYC_Plan_Projet_MVP.md épic E17
  • Feedback terrain VitaKYC (avril 2026)

ADR-008 — Génération PDF : Apache PDFBox + Gotenberg pour rendus riches

Section intitulée « ADR-008 — Génération PDF : Apache PDFBox + Gotenberg pour rendus riches »

Statut : Proposé · Date : 2026-04-22

Plusieurs artefacts PDF à produire : formulaires W-8BEN / W-8BEN-E / W-9, résumés KYC, rapports d’audit BCT/AMF, bordereaux FATCA + AD. Options : PDFBox (OSS Apache 2.0), iText (AGPL ou licence payante), wkhtmltopdf (archivé 2023), Puppeteer/Playwright (coût RAM), Gotenberg (service Chromium + LibreOffice en docker).

  • Apache PDFBox pour les formulaires réglementaires figés (W-8 / W-9, templates IRS) — contrôle précis des champs et fidélité visuelle.
  • Gotenberg comme service docker isolé pour les rendus riches (rapports stylés, dashboards exportables) — HTML + CSS print + graphiques via API simple.
  • Apache 2.0 compatible SaaS commercial (pas iText AGPL).
  • Gotenberg isole Chromium sans embarquer Puppeteer dans chaque service.
  • Couverture complète en deux outils simples.

iText AGPL / commercial cher · wkhtmltopdf mort · Puppeteer embarqué → empreinte RAM non maîtrisée.


ADR-009 — i18n : ICU MessageFormat + CSS logique + RTL arabe de bout en bout

Section intitulée « ADR-009 — i18n : ICU MessageFormat + CSS logique + RTL arabe de bout en bout »

Statut : Proposé · Date : 2026-04-22

VitaKYC est MENA-first. L’arabe RTL est une langue de première classe, pas une traduction tardive. Doit couvrir : back-office React, SDK Web, emails / SMS / PDFs, messages API, formats (dates Hijri optionnel, chiffres latin/indo-arabes, monnaies).

  • ICU MessageFormat (pluralisation, sélection, formats imbriqués) partout.
  • Front React : react-intl / FormatJS.
  • Backend Kotlin : kotlinx-serialization + formatter custom ICU.
  • CDLR data pour formats locaux.
  • CSS logique (margin-inline-start, padding-inline-end, text-align: start) pour tout nouveau composant.
  • dir="rtl" sur <html> activé par la locale ; chaque composant validé en miroir dans Storybook.
  • Polices : Inter (latin) + IBM Plex Sans Arabic (chargement conditionnel).
  • OpenSearch avec analyseurs arabic, french, english pour recherche multilingue.
  • BCP 47 (ar-TN, fr-TN, ar-SA, en-US).
  • ISO 4217 monnaies.
  • ISO 8601 dates/heures en API.

Conformité culturelle forte = différenciation MENA réelle. Coût : test de chaque composant UI en RTL.


ADR-010 — Distribution SDKs : NPM + Maven Central + Swift PM (public) + registries privés (enterprise)

Section intitulée « ADR-010 — Distribution SDKs : NPM + Maven Central + Swift PM (public) + registries privés (enterprise) »

Statut : Proposé · Date : 2026-04-22

SDKCanal publicCanal enterprise
Web TypeScript@vitakyc/sdk-web sur npmjs.comGitHub Packages privé
iOS SwiftSwift Package Manager depuis repo publicCocoaPods privé (spec repo)
Android KotlinMaven Central io.vitakyc:sdk-androidJFrog Artifactory privé
React Native@vitakyc/sdk-react-native
Flutter (V2)pub.dev vitakyc

Publication automatisée via CI tag-triggered (vX.Y.Z) depuis le monorepo SDK. SemVer strict, cosign sur artefacts, SBOM CycloneDX joint.

6 mois minimum de support après release d’une MAJOR. Headers Deprecation + doc changelog publique.

Self-hosted only (frein adoption), JitPack Android (peu pro).


ADR-011 — E-signature : DocuSign + Yousign FR + TunTrust/ANCE (Tunisie) + module natif SES + option QES

Section intitulée « ADR-011 — E-signature : DocuSign + Yousign FR + TunTrust/ANCE (Tunisie) + module natif SES + option QES »

Statut : Proposé · Date : 2026-04-22 · Révision : 2026-04-22 (ajout Tunisie)

Les formulaires W-8BEN / W-8BEN-E / W-9, les consentements de traitement et les contrats de délégation nécessitent une signature électronique. Enjeux :

  • Couverture géographique MENA (en particulier Tunisie) + UE + mondial
  • Conformité au cadre juridique local (force probante devant tribunal)
  • Coût par signature
  • Traçabilité intégrée à la piste KYC

Approche multi-provider configurable par tenant avec 5 options :

  1. DocuSign — provider par défaut Growth / Enterprise, couverture mondiale, API mature.
  2. Yousign / Universign (France) — eIDAS AES / QES, moins cher que DocuSign sur la zone UE.
  3. TunTrust / ANCE (Tunisie)NOUVEAU — signature électronique qualifiée tunisienne conforme à la loi n°2000-83 du 9 août 2000 relative aux échanges et au commerce électroniques. TunTrust (filiale de La Poste Tunisienne) est émetteur principal des certificats qualifiés, reconnus par tribunaux tunisiens et utilisables par les banques, assurances et administrations.
  4. Module natif VitaKYC — signature simple (SES) intégrée au parcours KYC pour les cas standards (consentement GDPR, W-8BEN individu) — évite le coût externe de 2-5 €/signature. Basé sur canvas HTML5 + timestamp + hash + piste audit WORM.
  5. Option QES on-prem — Cryptolog Evidency, Signaturit ou Universign QES pour clients exigeant PKI dédiée.
  • Infrastructure déjà requise : le cahier des charges DGI V1.0-2019 FATCA exige un certificat ANCE pour l’accès à la plateforme IDES (cf. §12.3.2 du cahier des charges VitaKYC). Les clients banque / IF tunisiens possèdent déjà ces certificats.
  • La Poste Tunisienne est actionnaire de TunTrust — alignement stratégique fort avec notre AO cible N°1.
  • Force probante locale : les contrats bancaires et consentements KYC signés avec TunTrust sont reconnus devant les tribunaux tunisiens, contrairement à DocuSign ou Yousign FR qui supposent un rattachement à une juridiction étrangère.
  • Écosystème TEIF — TunTrust/ANCE porte aussi les signatures TEIF (Tunisian Electronic Invoice Format) utilisées pour la facturation électronique. Supporter TunTrust à travers VitaKYC ouvre une optionalité V2/V3 pour un module e-invoicing TEIF (nouveau produit adjacent).
  • API : TunTrust expose une API de signature distante (Remote QES) via SOAP/REST selon offre ; alternative : client-side avec TUN-PKCS11 pour utilisation de cartes à puce / tokens USB ANCE-issued.
  • Certificats acceptés :
    • TunTrust Certificat Qualifié Personne Physique (pour signataires individuels dirigeants).
    • TunTrust Certificat Qualifié Personne Morale (cachet électronique IF déclarante).
    • Autres CAs reconnus par ANCE (SOTRACT, autres).
  • Formats produits : PAdES (PDF), XAdES (XML — utile pour FATCA XML v2.0 et TEIF), CAdES (CMS/PKCS7).
  • Validation : service de validation long-terme (LTV) pour garantir la validité dans le temps.
  • Stockage VitaKYC : les signatures et certificats associés sont archivés WORM 10 ans + horodatage qualifié.
CasNiveau exigéProvider recommandé
Consentement GDPR au KYCSESModule natif VitaKYC
W-8BEN individuSES + timestampModule natif VitaKYC
W-8BEN-E entité — tenant tunisienAES/QES localeTunTrust / ANCE
W-8BEN-E entité — tenant UEAESDocuSign / Yousign
Mandat SEPA — tenant FRAESYousign
Contrat compte bancaire TNQES tunisienneTunTrust / ANCE
Contrat compte bancaire FRQES eIDASYousign QES / Universign
Dépôt FATCA à IDES (cachet électronique IF)QES tunisienneTunTrust / ANCE (certificat obligatoire IDES)
Signature de rapport BCT généréQES tunisienneTunTrust / ANCE

Positives

  • Alignement fort avec le marché tunisien et les AO publics (La Poste Tunisienne en tête).
  • Force probante locale assurée.
  • Mutualisation de l’infrastructure PKI ANCE déjà requise pour IDES.
  • Ouvre une optionalité produit TEIF en V2/V3.

Négatives

  • 5 intégrations à maintenir (vs 3 initialement).
  • TunTrust API moins mature que DocuSign — documentation partielle, support local à cultiver.
  • Onboarding certificat ANCE par client ≈ 2-4 semaines côté client (pas de VitaKYC) — à intégrer au plan d’onboarding.
  • Module e-invoicing TEIF complet n’est pas dans le scope V1. Le support TunTrust permet de le construire en V2/V3 comme produit adjacent si un client en exprime le besoin.
  • Module de facturation B2G (Kassiopia, TTN) n’est pas dans le scope VitaKYC.

ADR-012 — Zero-downtime upgrades on-prem, y compris Oracle + support air-gap

Section intitulée « ADR-012 — Zero-downtime upgrades on-prem, y compris Oracle + support air-gap »

Statut : Proposé · Date : 2026-04-22

Clients banque on-prem exigent PostgreSQL ou Oracle. SLA 99,9 % requis. Certains environnements sont air-gap (pas de phone-home possible).

Stratégie blue-green au niveau pod + migrations DB backward-compatibles :

  1. Rolling update Kubernetes (maxUnavailable=0, maxSurge=1) par défaut.
  2. Flyway multi-SGBD (PostgreSQL + Oracle + SQL Server) avec règle absolue : toute release N+1 fonctionne avec le schéma N (pattern expand/contract).
  3. Tests CI obligatoires : image N+1 bootée sur schéma N avant approbation release.
  4. Runbook trimestriel on-prem livré avec chaque release, incluant pré-requis Oracle (privilèges, tablespaces, backups).
  5. Rollback documenté step-by-step, testé sur env client de test avant upgrade prod.
  6. Bundle air-gap signé cosign : images OCI + Helm chart + migrations SQL + release notes, transféré sur support amovible chiffré si requis.

Liquibase (plus complexe qu’on en a besoin MVP) · Schema-per-version (impossible multi-tenant Oracle) · Downtime toléré (inacceptable banque tier-1/2).


ADR-024 — Stratégie mobile : SDK-first + tenant resolution SaaS / on-prem / hybride

Section intitulée « ADR-024 — Stratégie mobile : SDK-first + tenant resolution SaaS / on-prem / hybride »

Statut : Accepté Date : 2026-04-23

Question remontée du fondateur : “Comment l’application mobile sait à quel tenant se connecter en SaaS, et comment on traite les clients on-premise ?”

Trois risques réels derrière cette question :

  1. Confiance : le client final d’une banque télécharge-t-il une app “VitaKYC” ou l’app de sa banque ? Les utilisateurs font confiance à leur banque, pas à un tiers KYC. Publier une app VitaKYC grand public cannibaliserait la marque banque et créerait un risque d’hameçonnage (fake VitaKYC apps).
  2. App Store publishing : publier une app par tenant (white-label) impose à chaque banque de maintenir un compte Apple Developer ($99/an) + Play Console ($25 one-time), plus les cycles de review qui peuvent bloquer un go-live commercial.
  3. On-premise : un client on-prem ne doit jamais avoir ses flux mobiles transiter par vitakyc.com — contrainte BCT “pas de flux transfrontaliers non autorisés”.

Patterns de référence observés chez les concurrents :

ÉditeurApp standalone ?Pattern retenu
OnfidoNon (client)SDK embarqué dans app banque
JumioNon (client)SDK + Web SDK PWA
SumsubNon (client)SDK + WebSDK + NFC module
VeriffNon (client)SDK + In-house flow web
IDnowNon (client)SDK + VideoIdent service web

Tous ont une app standalone réservée aux agents (back-office mobile), pas au client final.

Trois surfaces mobiles VitaKYC, aucune app “VitaKYC” standalone grand public.

#SurfacePublisher storeUtilisateurVecteur de distribution
1SDK natif iOS/Android + React Native + FlutterLa banque (dans son app)Client final KYCPackage registry (Maven Central, Swift PM, CocoaPods, NPM)
2Web SDK / PWA branded— (URL)Client final KYC sans app banque (ou iOS sans NFC)Lien SMS / QR / magic-link
3VitaKYC Agent Mobile (standalone)VitaKYCAgents back-office, superviseurs, compliance officers en mobilitéApple App Store + Google Play + enterprise distribution (MDM Jamf/Intune) pour tenants sensibles

Le tenant_id est figé au build time côté SDK client, résolu à runtime côté Agent Mobile (login → tenant du compte), et porté par le domaine + JWT signé côté Web SDK.

// Intégration dans l'app banque — BuildConfig.kt
VitaKYC.configure(
tenantId = BuildConfig.VITAKYC_TENANT_ID, // "TN-BANQUEX" — figé au build
apiBase = BuildConfig.VITAKYC_API_BASE, // "https://api-tn.vitakyc.com"
pinnedSha = BuildConfig.VITAKYC_CERT_SHA256, // cert pinning anti-MITM
sessionProvider = { bankBackend.issueKycToken(userId) } // JWT 30 min signé banque
)
  • Clés baked dans BuildConfig (Android) / Info.plist (iOS) / react-native-config → aucun switching tenant à runtime, aucune ambiguïté.
  • sessionProvider retourne un JWT signé par le backend banque : { sub, tenant_id, kyc_session_id, risk_context, exp }.
  • L’API VitaKYC valide la signature via JWKS publiée par la banque (OpenID Connect discovery).

Identique au SaaS partagé, mais apiBase pointe vers l’instance dédiée : https://<banquex>.kyc.vitakyc.com.

Le SDK ne parle jamais à vitakyc.com. Il parle au mobile gateway déjà existant de la banque, qui reverse-proxy vers VitaKYC on-prem en zone interne sécurisée.

[ App Banque X sur mobile client ]
│ HTTPS + cert pinning = cert banque
[ Mobile gateway Banque X — DMZ ]
│ mTLS interne
[ VitaKYC on-prem — zone sécurisée bancaire ]
  • apiBase = "https://mobile.banquex.tn/kyc"domaine banque, pas vitakyc.com
  • Cert pinning = cert banque
  • Aucun flux sortant mobile → VitaKYC.com → conforme exigence BCT
  • Aucune règle firewall nouvelle à ouvrir (port 443 mobile banking existe déjà)
  • Côté VitaKYC on-prem : mode mono-tenant, header X-Tenant-Id validé statique

Patterns — hybride (holding bancaire multi-pays)

Section intitulée « Patterns — hybride (holding bancaire multi-pays) »

Banque régionale MENA avec une seule app, clients dans plusieurs pays, résidence de données par pays.

1. App banque → GET https://banquex.com/me/region (backend banque)
2. Réponse : { region, tenant_id, kyc_endpoint }
3. App banque → VitaKYC.configure(...) avec valeurs dynamiques

La banque route son client vers la bonne région VitaKYC — elle connaît son client (IBAN, résidence, produit), pas VitaKYC.

Patterns — fallback PWA (client sans app banque)

Section intitulée « Patterns — fallback PWA (client sans app banque) »
Agent en agence → "Créer session onboarding"
↓ POST /api/v1/onboarding/sessions
VitaKYC → { session_token (JWT 30min), url: "https://kyc.banquex.tn/s/ABC123" }
↓ SMS au client
Client → Safari/Chrome → kyc.banquex.tn (CNAME → vitakyc-cdn.com)
↓ reverse lookup Host header OU claim JWT
Tenant résolu → PWA branded charge → flow KYC identique SDK natif

Limite iOS : pas de NFC eMRTD en web (Web NFC = Android Chrome only). Si NFC obligatoire → SDK natif seul chemin.

CoucheMécanismeRôle
L7 ingress (Traefik / nginx)Host header + path prefixRoute vers namespace K8s tenant-TN-BANQUEX (SaaS shared)
API gateway (Kong / Envoy)Claim tenant_id du JWTEnforce RBAC + rate limit par tenant
Service applicatiftenant_id context propagé via Kotlin MDC + WebFilterChaque requête SQL inclut WHERE tenant_id = $1 (RLS PostgreSQL, cf. ADR-002)

Le header X-Tenant-Id est accepté mais jamais trusté : cross-vérifié avec le claim JWT. Mismatch → 401 + alerte SIEM (tentative cross-tenant).

Ce qui ne doit JAMAIS fuiter dans un bundle mobile

Section intitulée « Ce qui ne doit JAMAIS fuiter dans un bundle mobile »
  • ❌ Clé API VitaKYC — jamais, c’est un secret serveur
  • ❌ Credentials DB tenant — jamais
  • ❌ JWT long-lived — jamais, max 30 min, émis par backend banque
  • tenantId (public — c’est un routing hint)
  • ✅ Cert SHA256 pinning (public)
  • ✅ JWT éphémère 30 min

Hypothèse sécurité : le SDK mobile est toujours considéré compromis (root/jailbreak, reverse engineering Frida/Hopper). Aucun secret long-lived. Seul JWT éphémère signé par backend banque après auth forte.

Positives

  • UX préservée : le client reste dans son app banque, pas de cassure de confiance.
  • Pas de gestion App Store multi-tenant — les banques publient leurs propres apps comme elles le font déjà.
  • On-prem réellement on-prem : zéro flux mobile sortant vers vitakyc.com, exigence BCT satisfaite.
  • Cert pinning par tenant → isolation cryptographique des tenants jusque sur le device.
  • Alignement avec la pratique industrie (Onfido, Jumio, Sumsub, Veriff).

Négatives

  • Pas de mise à jour “push” du SDK côté client — chaque upgrade SDK = publication nouvelle version de l’app banque (review stores 1-7j). Mitigation : remote config (règles + textes + flags) servi à runtime via /config/runtime pour modif sans rebuild.
  • Intégration = effort côté banque (2-4 sprints typiques). Mitigation : SDK quickstart + CSM VitaKYC accompagné.
  • Web SDK iOS sans NFC → parcours dégradé. Mitigation : documenté, fallback selfie + doc photo + liveness + controls AML compensatoires.
  • SDK size budget : iOS ≤ 12 MB, Android ≤ 8 MB (baseline), modèles ONNX liveness/quality téléchargés à la première ouverture via CDN tenant-signé (pas bundled). Voir ADR-016 (à affiner).
  • Lazy loading : module NFC iOS, module VideoKYC chargés à la demande, pas au cold start.
  • Offline mode : capture photos + liveness → upload différé avec retry backoff exp (1s, 2s, …, cap 60s), expiration session 30 min côté serveur.
  • Device attestation : Play Integrity (Android) + DeviceCheck/App Attest (iOS) — device rooted/jailbroken → peut passer en risk_high pour review manuelle obligatoire (policy par tenant).
  • App Agent Mobile distribuée aussi en enterprise MDM (Jamf, Intune) pour tenants refusant App Store public (cas BCT tier-1).
CasComportement
Bascule de tenant à chaudInterdite — réinstallation de l’app banque
Session expirée pendant l’onboardingRefresh silencieux via sessionProvider; sinon écran “reprise ultérieure” avec deeplink
Cert pinning fail (proxy entreprise)Refus connexion, message clair, lien support
Deep-link VitaKYC SDK ≠ user courantIntent handler vérifie sessionToken.sub == currentBankUser, sinon logout forcé
Downgrade HTTPHSTS PWA + NSAppTransportSecurity strict iOS + cleartextTrafficPermitted="false" Android
Device rooted/jailbrokenFlag device_risk=high, décision policy tenant (bloquant ou review manuelle)
App en arrière-plan pendant livenessReprise caméra impossible sur iOS → user recommence cette étape uniquement (state saved)
OptionPourquoi écartée
App VitaKYC standalone grand public + saisie code tenantCassure de confiance, risque hameçonnage (fake VitaKYC apps), cannibalisation marque banque.
App white-label republiée par tenant (one app per bank)Cycle App Store review par tenant = go-to-market bloquant ; coût $99/an × N tenants + gestion certs signing démultipliée.
Web SDK uniquement, pas de SDK natifPas de NFC eMRTD iOS, perf caméra dégradée, rejet des banques tier-1 qui veulent expérience native.
App mobile unique VitaKYC avec tenant switching au loginInutilisable : le client n’est pas un utilisateur qui “se connecte à un tenant”, il complète un onboarding unique pour un produit unique d’une banque unique.
  • Revue à fin S08 : premier tenant en production (La Poste Tunisienne ou équivalent) avec SDK intégré dans leur app. Validation : temps d’intégration, feedback agent CSM.
  • Revue M+12 : consolidation tenants + retour ops sur pinning + rotation certs + adoption Web SDK vs natif.
  • Trigger d’un nouvel ADR : si ≥ 2 tenants demandent un mode non prévu (ex : tenant switching multi-compte intra-banque), révision de la stratégie.
  • Pattern industrie : Onfido iOS/Android SDK docs, Jumio NetVerify SDK, Sumsub MobileSDK
  • BCT Circulaire 2017-08 §III (résidence des données)
  • RGPD art. 25 (Privacy by Design)
  • NIST SP 800-63B (device assurance levels)
  • VitaKYC_Cahier_des_Charges.md §5 (architecture) et §7 (sécurité)
  • ADR-002 (multi-tenant RLS), ADR-010 (distribution SDKs), ADR-011 (e-signature)
  • Page engineering : Mobile SDK — intégration tenant SaaS / on-prem

ADR-025 — Modèle de risque client : matrice multi-dimensionnelle + RBA paramétrable

Section intitulée « ADR-025 — Modèle de risque client : matrice multi-dimensionnelle + RBA paramétrable »

Statut : Accepté Date : 2026-04-23

Question remontée du fondateur : “Qu’est-ce qu’on fait avec les business rules et les matrices de risques ? Est-ce qu’elles doivent être intégrées dans une solution KYC ?”

Oui, c’est bloquant, pour trois raisons :

  1. Obligation réglementaire — l’approche par les risques (Risk-Based Approach, RBA) est imposée par :
    • FATF Reco. 10 — identification et classification des risques BC/FT obligatoire
    • Directive UE 6AMLD art. 18 — CDD simplifiée / standard / renforcée selon le risque
    • Tunisie · Circulaire BCT 2017-08 art. 8 — classification client obligatoire
    • Tunisie · Loi 2015-26 (LCB-FT) art. 99-106 — revue périodique du profil
    • FinCEN CDD Rule — ongoing monitoring based on risk
    • BaFin KWG §25h — Klassifizierung des Geschäftsrisikos En audit, le régulateur exige la trace structurée de la classification de chaque client. Sans ça : amende jusqu’à 10% PNB + sanction du dirigeant.
  2. Différenciation commerciale — tous les concurrents KYC font OCR et liveness. Ce qui fait gagner l’AO : “est-ce que je peux encoder MA politique, pas la vôtre ?”. Sans moteur de règles paramétrable, VitaKYC perd les AO tunisiens et européens.
  3. Soutenabilité opérationnelle — sans scoring automatique, 100% des dossiers sont traités manuellement. Coût / vérif explose (15-30 DT vs 0,5-2 DT), SLA STR explose. Avec RBA bien calibré : 60-75% auto-approve sur LOW, EDD obligatoire sur HIGH, refus auto sur PROHIBITED.

L’ADR-004 a déjà décidé du moteur d’exécution (DSL Kotlin + OPA). Cet ADR décide de la structure du modèle de risque qui s’exécute dans ce moteur.

VitaKYC livre un risk engine paramétrable par tenant basé sur :

  1. Matrice multi-dimensionnelle — 5 dimensions standard industrie (Wolfsberg / FATF) :
    • client (PEP, profession, résidence, âge, statut matrimonial)
    • geo (pays résidence, pays nationalité, FATF greylist/blacklist, CPI Transparency, sanctions pays)
    • product (type produit, montants attendus, crypto, offshore)
    • channel (face-à-face vs remote, QES vs OTP, agent vs self)
    • aml_screening (PEP hit, sanctions hit, adverse media) Chaque dimension = score pondéré 0-100, pondérations et seuils paramétrables par tenant.
  2. 4 niveaux de risque — LOW [0-25] · STANDARD [26-50] · HIGH [51-80] · PROHIBITED [81-100]
  3. Override rulesmustProhibit, mustHigh, mustLow qui outrepassent le score calculé (ex : match OFAC = PROHIBITED quel que soit le reste)
  4. Explainability obligatoire — chaque décision émet un JSON structuré : contribution de chaque dimension, raison, règles déclenchées, mitigations requises
  5. Cycle de vie discipliné :
    • versioning immuable par publication
    • dual control (compliance + DSI) obligatoire pour publier
    • shadow mode (nouvelle version en parallèle sans impact décisions) avant bascule
    • backtesting automatique sur 6 derniers mois
    • revue annuelle obligatoire (BCT + FATF Reco 1)
    • audit log WORM (qui a modifié, quand, approuvé par qui)
  6. Mapping niveau → politique — chaque niveau impose :
    • le niveau de CDD (simplifiée / classique / renforcée / refus)
    • les pièces obligatoires
    • la chaîne de décision (auto / agent / agent+superviseur / compliance)
    • la périodicité de revue (5 ans / 3 ans / 1 an / 6 mois)
    • le SLA STR (N/A / J+10 / J+5 / J+2)
riskPolicy("TN-BANQUEX", version = "2.1") {
dimension("client", weight = 0.30) {
score(100) whenMatch { client.pep == PepStatus.OWN }
score(75) whenMatch { client.pep in setOf(PepStatus.FAMILY, PepStatus.CLOSE_ASSOC) }
score(50) whenMatch { client.profession in HIGH_RISK_PROFESSIONS }
score(0) otherwise
}
// ... autres dimensions
threshold(LOW to STANDARD, at = 25)
threshold(STANDARD to HIGH, at = 50)
threshold(HIGH to PROHIBITED, at = 80)
mustProhibit whenAny {
screening.ofac_match == TRUE_POSITIVE
client.country in listOf("KP", "IR")
}
}

Le POC poc-risk-engine (voir page dédiée) démontre ce DSL end-to-end avec 10+ tests sur profils BCT typiques.

Positives

  • Conformité auditable out-of-the-box pour BCT, AMF, BaFin, FinCEN
  • Chaque tenant encode sa politique sans intervention dev VitaKYC
  • Explainability satisfait RGPD art. 22 (droit à l’explication des décisions automatisées)
  • Backtesting + shadow mode limitent drastiquement les risques de régression compliance
  • Réutilisable par le module AML Transaction Monitoring (scoring transactionnel = 6ème dimension additionnelle)
  • Différenciation claire vs Onfido / Jumio (qui ne livrent pas de RBA aussi structurée)

Négatives

  • Complexité UI : le risk editor est un écran d’admin non-trivial (cf. workflow 10 mockups)
  • Courbe d’apprentissage compliance : les officers doivent être formés au DSL ou passer par une UI visuelle (les deux sont offerts)
  • Charge backtesting : rejouer 6 mois de dossiers = traitement batch non-négligeable → Temporal workflow dédié avec throttling
  • Tentation surcharge : certaines banques voudront 20 dimensions — on cap à 8 au MVP pour garder des perfs < 50ms p99
  • Performance : évaluation synchrone doit tenir en < 50 ms p99. Cache statique des listes FATF/OFAC en mémoire, reload incrémental via Kafka. Pas de DB lookup dans l’hot path.
  • Stockage : policies en PostgreSQL JSONB + versioning (table append-only). Évaluations (décisions) en table WORM séparée (retention 10 ans BCT).
  • API : REST POST /v1/risk/evaluate (synchrone) + événement Kafka risk.evaluated (asynchrone, consommé par AML TxMon et case management).
  • Format DSL : Kotlin au backend + représentation JSON équivalente pour édition UI (round-trip parfait). Le compliance officer n’écrit pas du Kotlin — il utilise l’UI (workflow 10), qui sérialise en JSON, validé et compilé en DSL Kotlin à la publication.
  • Model cards : chaque dimension a une model card (objectif, variables, références réglementaires, revue, owner). Obligatoire pour l’audit.
  • Overrides bloquants uniquement — un override peut FORCER un niveau plus élevé (mustProhibit, mustHigh) mais jamais ABAISSER. Règle métier : “on peut être plus prudent qu’un score, jamais moins”.
  • Données sensibles : le profil client utilisé pour l’évaluation inclut PII → évaluation dans le tenant, jamais de cross-tenant. Row-Level Security PostgreSQL (ADR-002) + tenant_id propagé en MDC.
  • Intégration Form Designer via Client Profile Schema (amendement 2026-04-23, cf. ADR-026) — l’éditeur de règle ne référence jamais une “chaîne libre” : il consomme exclusivement les variables publiées dans le CPS du tenant. Autocomplete depuis le CPS, refus HTTP 400 d’une règle référant un symbole inexistant, vue “Champs consommés” par dimension avec lien vers le Form Designer propriétaire. Les tentatives de publication d’une policy référençant un champ déprécié sont bloquées avec 90j de fenêtre de migration.
OptionPourquoi écartée
ML end-to-end pour scoringNon auditable par BCT, droit à l’explication RGPD difficile, données d’entraînement insuffisantes en MENA au MVP. ML utilisé en complément (scoring adverse media, détection anomalies transactionnelles) mais pas pour le scoring RBA principal au MVP.
Pas de DSL, règles codées en durImpossible de vendre (chaque banque a sa politique). Releases mensuelles au rythme des banques = impossible.
Drools / moteur commercialLicences Oracle/Red Hat coûteuses, compétence rare, DSL peu lisible par les compliance officers. Kotlin type-safe + UI est plus maintenable.
Fusion totale dans le Form DesignerÉcarté : confusion des responsabilités (produit vs conformité), cycles incompatibles (hebdo vs annuel), audiences différentes (admin tenant vs compliance + DSI), exigences d’audit différentes (dual control + WORM 10 ans vs 5 ans). Intégration via contrat CPS retenue (voir ADR-026).
4 dimensions seulement (pas d’aml_screening)Rejeté parce que le hit OFAC / sanction doit pouvoir driver la décision même si le client est parfait par ailleurs.
  • Démo fin S04 : DSL + UI visuelle + backtesting fonctionnel sur 3 profils types.
  • Pilote fin S10 : première matrice BCT calibrée avec un client pilote tunisien.
  • Revue M+6 : taux auto-approve LOW mesuré (cible ≥ 60%), false positive HIGH (cible ≤ 5%), SLA évaluation p99 (cible ≤ 50 ms).
  • Trigger nouvel ADR : si un tenant demande un 6ème axe permanent OU si on introduit de l’adaptive ML sur les pondérations.
  • FATF Recommendations (février 2012, révisées octobre 2023), Reco. 1, 10, 12, 15
  • Wolfsberg Group — Guidance on a Risk Based Approach (2015)
  • BCT Circulaire 2017-08 — art. 8 (approche par les risques) et annexe D (classification type)
  • Loi tunisienne 2015-26 art. 99-106 (revue périodique)
  • RGPD art. 22 (décisions automatisées + droit à l’explication)
  • Bâle — “Sound management of risks related to money laundering and financing of terrorism” (révisé 2020)
  • ADR-002 (multi-tenant RLS), ADR-004 (DSL + OPA), ADR-007 (Form Designer scope), ADR-026 (intégration CPS)
  • Page ingénierie : Risk Engine
  • Page ingénierie : Client Profile Schema
  • POC : poc-risk-engine
  • POC : poc-cps-registry
  • Playbook : Calibrer et auditer la matrice BCT

ADR-026 — Intégration Form Designer ↔ Risk Matrix via Client Profile Schema partagé

Section intitulée « ADR-026 — Intégration Form Designer ↔ Risk Matrix via Client Profile Schema partagé »

Statut : Accepté Date : 2026-04-23

Question remontée du fondateur : “Est-ce que les matrices de risques doivent être intégrées avec le Form Designer ?”

Trois niveaux d’intégration possibles ont été évalués :

OptionDescriptionVerdict
A. Fusion complèteLe Risk Matrix est un onglet du Form Designer (même UI, même data model, même publication)Écarté
B. Isolation totaleForm Designer et Risk Matrix s’ignorent mutuellementÉcarté
C. Loose coupling via contrat partagé (CPS)Deux UIs, deux lifecycles, mais un catalogue canonique de variables communRetenu

La fusion casse cinq invariants critiques :

  1. Audiences distinctes : Form Designer = admin tenant + CSM VitaKYC (produit/UX). Risk Matrix = compliance officer + directeur compliance + DSI (conformité). Les banques tier-1 refuseront qu’un admin produit puisse accidentellement modifier une policy de risque.
  2. Cycles de publication différents : Form Designer cadence produit (hebdo/mensuel), Risk Matrix cadence conformité (annuel + trigger events). Fusionner met la compliance au rythme du produit → régressions garanties.
  3. Exigences d’audit différentes : Risk Matrix requiert dual control + model cards + shadow mode + backtesting + audit WORM 10 ans + droit à l’explication RGPD art. 22. Form Designer n’exige rien de tout ça.
  4. Sémantiques distinctes : Form Designer répond à “qu’est-ce qu’on DEMANDE ?”, Risk Matrix à “comment on DÉCIDE ?”. Les mélanger crée un anti-pattern d’architecture.
  5. Réutilisabilité : Risk Matrix raisonne aussi sur les données KYB (RNE), screening (PEP/sanctions/media), transactionnelles (AML TxMon) — pas seulement sur un formulaire. Le coller au Form Designer casse ces consommations.

L’isolation totale casse la référentielle de manière silencieuse :

  • Un admin supprime un champ Form Designer → règle de risque l’utilisant devient muette → plus jamais de true positive, personne ne s’en aperçoit.
  • Un compliance officer écrit client.homeowner_status > 0 sur un champ inexistant → la règle passe la validation syntaxique mais ne matche jamais.
  • Un champ renommé côté Form → rule fail silencieusement en prod.
  • Le compliance officer ne sait pas qu’un nouveau champ a été ajouté côté form → il ne l’utilise jamais.

Intégration via contrat partagéClient Profile Schema (CPS).

Le CPS est un catalogue canonique versionné des variables observables par tenant. Chaque source (Form Designer, connecteur RNE, module screening, AML TxMon) y déclare ses variables. Chaque consommateur (principalement le Risk Matrix) y réfère sans jamais “chaîne libre” ambigu.

┌────────────────────────────────────────────────────────────┐
│ Client Profile Schema (CPS) — contrat canonique │
│ │
│ client.* ← Form Designer (+ screening enrichment) │
│ entity.* ← Form Designer + connecteur RNE │
│ product.* ← catalogue produit banque │
│ session.* ← runtime SDK mobile/web │
│ screening.* ← module AML screening │
│ transactional.* ← AML TxMon (quand activé) │
└───────────┬──────────────────────────────┬─────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Form Designer │ │ Risk Matrix │
│ déclare + UI │ │ consomme + règles│
└──────────────────┘ └──────────────────┘

Côté Form Designer

  • Chaque champ déclare son mapping CPS (client.profession, client.country, etc.)
  • Badge “Utilisé par N règles de risque” sur chaque champ référencé
  • Refus de suppression / renommage si le champ est utilisé par une policy ACTIVE ou SHADOW
  • Publication d’une nouvelle version du form → event form.published consommé par le CPS → compliance notifié : “3 champs ajoutés, 1 renommé. Impact Risk Matrix à évaluer.”

Côté Risk Matrix

  • L’éditeur de règle autocomplete depuis le CPS (tape client. → liste des 23 variables disponibles par tenant)
  • Validation serveur : règle référençant un symbole inexistant → refus HTTP 400
  • Vue “Champs consommés” sur chaque dimension, liens vers Form Designer + autres sources
  • Alerte quand un champ référencé est déprécié ou retiré

Côté CPS (nouveau micro-service léger profile-schema-svc)

  • Source de vérité des variables disponibles par tenant
  • Versionné append-only (cadencé par les événements sources)
  • API REST GET /v1/cps/tenant/{id} → schéma courant avec origine de chaque variable (client.profession vient de form:FORM_KYC_INDIVIDUAL@v2.7)
  • Consomme Kafka : form.published, screening.enabled, rne.connected, aml-txmon.enabled, etc.
  • Ne stocke aucune PII — seulement des noms, types, contraintes, model card
{
"tenant_id": "TN-BANQUEX",
"version": "2026-04-23T11:42:00Z",
"variables": [
{
"path": "client.profession",
"type": "enum",
"enum_catalog": "BCT_PROFESSION_CODES",
"source": "form:FORM_KYC_INDIVIDUAL@v2.7",
"required": false,
"sensitivity": "PII",
"deprecated": false,
"used_by": [
{ "kind": "risk.rule", "ref": "policy:TN-BANQUEX@2.1#dimension=client#rule=3" },
{ "kind": "risk.rule", "ref": "policy:TN-BANQUEX@2.1#dimension=client#rule=5" }
]
},
{
"path": "client.country",
"type": "iso3166-alpha2",
"source": "form:FORM_KYC_INDIVIDUAL@v2.7",
"required": true,
"sensitivity": "PII",
"used_by": [
{ "kind": "risk.rule", "ref": "policy:TN-BANQUEX@2.1#dimension=geo" },
{ "kind": "risk.override", "ref": "policy:TN-BANQUEX@2.1#mustProhibit=2" }
]
}
]
}

Positives

  • Référentielle garantie → aucun champ fantôme, aucune règle orpheline
  • Chaque module garde son lifecycle, son audit, sa gouvernance indépendante
  • Notifications cross-module automatiques (nouveau champ → compliance alerté)
  • Réutilisable pour les futurs modules (AML TxMon, KYB, OSINT)
  • Aligné avec la pratique industrie (Onfido “applicant profile schema”, Persona “inquiry template”, Sumsub “user level”)
  • Validations serveur (HTTP 400) empêchent la publication d’une policy référant un symbole inexistant

Négatives

  • Micro-service supplémentaire à monter (profile-schema-svc) au S03
  • Contrat à maintenir + migration lors d’évolutions (deprecations)
  • UX supplémentaire sur le Form Designer (badges, blocage suppression) → coût front
  • Version bump CPS à chaque publication Form Designer → peut inonder les events Kafka sur les tenants qui itèrent vite → mitigation via coalescing
  • Ownership CPS : la plateforme (VitaKYC), pas les tenants. Le tenant peut seulement ajouter via ses sources déclaratives (Form Designer, activation module).
  • Évolution : deprecation soft d’abord (warning) puis hard (rejet après fenêtre de 90j). Jamais de rupture sans alertes préalables.
  • Retention : 10 ans WORM (même régime que Risk Matrix) pour garantir la reproductibilité des backtests sur historique long.
  • Scalabilité : CPS est petit (~KB par tenant), read-heavy. Cache Caffeine local à chaque service consommateur. Refresh sur event Kafka.
  • Sécurité : pas de PII dans le CPS, seulement des noms et métadonnées. Accès lecture : tous les services du tenant. Écriture : seulement via events Kafka signés (HMAC + origin whitelist).
  • Multi-tenant : CPS segmenté par tenant (row-level security ADR-002). Jamais de leak cross-tenant.
OptionPourquoi écartée
Fusion Form Designer + Risk Matrix (option A)Casse audiences, cycles, audits, sémantique, réutilisabilité.
Isolation totale (option B)Casse référentielle → règles orphelines silencieuses, champs fantômes, bugs subtils en prod.
Schéma dans PostgreSQL partagé sans service dédiéFonctionne mais manque d’API claire, de versioning discipliné, d’events Kafka. Déplacement vers un service léger au S04 de toute façon.
GraphQL federated schemaOverkill pour la taille du contrat (~KB) et la simplicité des consommateurs. Ajoute du tooling (gateway, federation spec) sans bénéfice ici.
JSONSchema standard strictConsidéré — utilisé en interne pour valider le contrat. Mais pas suffisant : il faut en plus la notion de source, usedBy, deprecated, sensitivity → extension custom.
  • Démo fin S04 : CPS opérationnel, Form Designer déclare les champs, Risk Matrix consomme avec autocomplete fonctionnel.
  • Pilote S10 : test référentielle en conditions réelles — admin tente de supprimer un champ, Form Designer bloque correctement.
  • Revue M+6 : métriques — % de règles impactées à chaque publication de form, % alertes compliance auto-traitées, nombre de cross-tenant leaks (doit être 0).
  • Trigger nouvel ADR : si un 3ème consommateur majeur émerge (ex : un moteur ML qui consomme aussi le CPS) → revoir la structure du contrat.
  • Patterns industrie : Onfido Applicant Profile, Persona Inquiry Template, Sumsub User Level
  • JSON Schema 2020-12 (utilisé en interne pour validation)
  • ADR-002 (multi-tenant RLS), ADR-007 (Form Designer), ADR-025 (Risk Matrix)
  • Page engineering : Client Profile Schema
  • POC : poc-cps-registry
  • Workflow 3 mockups (Form Designer) : badge “Used by X rules” + deletion guard
  • Workflow 10 mockups (Risk Matrix) : autocomplete + vue champs consommés

ADR-027 — Form Designer : moteur d’exécution déclaratif (JSON canonique + DSL Kotlin + runtime isomorphe)

Section intitulée « ADR-027 — Form Designer : moteur d’exécution déclaratif (JSON canonique + DSL Kotlin + runtime isomorphe) »

Statut : Accepté Date : 2026-04-25

ADR-007 a fixé le scope du Form Designer (no-code MVP, drag-drop, i18n FR/AR/EN, versioning). ADR-026 a fixé le contrat externe avec le Risk Matrix via le CPS. Reste à fixer le moteur d’exécution : représentation interne, modèle de règles, interaction designer/runtime, contraintes à la publication, propagation des versions.

Trois forces sont en tension :

  1. Lisibilité auditeur (BCT) — la définition d’un formulaire et de ses règles doit être lisible, diffable, signable. Les politiques sont versionnées 10 ans WORM (cf ADR-002).
  2. Performance runtime — le SDK Web (mobile + desktop) charge la définition active et évalue les règles localement sans round-trip serveur, pour ne pas augmenter la latence ni l’attaque réseau pendant le KYC.
  3. Cohérence cross-langage — designer et SDK doivent évaluer les règles identiquement. Une condition de visibilité qui dit “afficher si revenus > 10 000 TND” ne peut pas évaluer différemment dans la preview Designer (JVM) et dans le browser client.

À cela s’ajoutent les invariants d’intégration CPS (ADR-026) et i18n (ADR-009).

Trois choix structurants :

1. Représentation : JSON canonique versionné — pas YAML, pas BPMN, pas binaire

Une FormDefinition est un document JSON déterministe (clés ordonnées, indentation 2 espaces, encodage UTF-8 NFC) avec un schéma fermé strict. Tout formulaire publié est un JSON immutable, hashé SHA-256, signé Ed25519 dual-control au publish (cf ADR-006 et risk-matrix dual-control).

FormDefinition
├── meta { formId, version, tenantId, hash, signature, publishedAt, publishedBy }
├── locales [ "fr", "ar", "en" ] // langues activées tenant
├── i18n { "<key>": { fr, ar, en } } // catalogue centralisé
├── steps [ StepDef ] // ordre conservé
├── rules [ Rule ] // règles de visibilité, required, skip, block, prefill
└── consent { stepId, version, hash } // référence consentement RGPD/LCB-FT

2. Construction : DSL Kotlin côté serveur — transpilation vers JSON canonique

Designers et intégrateurs avancés peuvent écrire en DSL Kotlin (cohérent ADR-004 et ADR-025) avec invariants require() enforcés au build. Le designer no-code génère le même JSON via un transpileur. Round-trip JSON ↔ DSL strict garanti par tests propriété.

val formKycIndividualV2 = formDefinition {
meta {
formId = "FORM_KYC_INDIVIDUAL"
tenantId = "TN-BANQUEX"
version = "2.7.0"
}
locales("fr", "ar", "en")
step("step_identity") {
title = i18n("step.identity.title") // FR : "Identité", AR : "الهوية", EN : "Identity"
field("first_name") {
type = STRING
cpsPath = "client.firstName"
required = true
label = i18n("field.firstName.label")
}
field("country") {
type = ISO3166_ALPHA2
cpsPath = "client.country"
required = true
}
}
rule("show_us_indicia") {
`when` { field("us_birth").eq(true) or field("us_address").eq(true) }
then = visible("step_us_indicia")
}
rule("block_minor") {
`when` { age("client.dob") lt 18 }
then = block(message = i18n("error.minor.notEligible"))
}
}

3. Runtime isomorphe : évaluateur sérialisable — même AST en Kotlin (JVM/Designer) et en TypeScript (SDK Web)

L’évaluateur de règles est pur (pas d’I/O, pas d’état), prend en entrée un FormDefinition + un état Submission partiel, retourne une Resolution ({ visible: Set<FieldId>, required: Set<FieldId>, skipped: Set<StepId>, blocked: Boolean, blockMessage?: string, prefilled: Map<FieldId, Value> }). Le code est généré depuis une spec d’AST commune (Predicate, Action) compilée en deux cibles :

  • form-engine-jvm (Kotlin) — utilisé par Designer, Validator publish, tests automatisés, génération PDF récap
  • form-engine-web (TypeScript port) — utilisé par SDK Web, embarqué dans l’iframe Shadow DOM tenant

Garantie d’équivalence par golden tests (1 corpus de 80 cas (definition, submission, expectedResolution)) exécutés sur les deux runtimes en CI. Tout drift de comportement = build rouge.

  • Lisibilité audit : JSON canonique diffable ligne à ligne, signature Ed25519 prouve l’intégrité, hash SHA-256 dans les events Kafka et le case audit trail.
  • Performance : évaluateur isomorphe = zéro RTT pendant l’évaluation des règles. Même un téléphone bas de gamme évalue 50 règles en < 5 ms (mesuré POC Risk Engine et Form Designer).
  • Cohérence cross-langage : la même AST sérialisée garantit que la preview Designer et le SDK Web donneront identique. Aucun if/else dupliqué entre Kotlin et TypeScript.
  • Réutilisation Risk Engine : le DSL Form Designer reprend exactement la grammaire de prédicats du Risk Engine (ADR-025) — même opérateurs (==, !=, <, >, <=, >=, in, contains, matches), mêmes accès field("..."), mêmes catalogues. Un développeur qui maîtrise l’un maîtrise l’autre.
  • Mapping CPS enforced à la compilation : le DSL exige cpsPath pour tout champ non-internal_only. Un formulaire qui omet cette mention casse à la compilation Kotlin, pas en runtime. Cohérent avec ADR-026.

Positives

  • Audit BCT facilité : une FormDefinition publiée est un artefact JSON signé reproductible.
  • Backtest possible : on peut rejouer une Submission historique avec n’importe quelle FormDefinition du passé pour comprendre l’origine d’un parcours.
  • Tooling diff : un PR sur le repo policies montre clairement les ajouts/suppressions/renames de champs.
  • SDK Web léger (< 60 KB gzip) — pas besoin d’un parseur YAML, juste un évaluateur AST minimal.

Négatives

  • Coût initial du double runtime (JVM + TS) — estimé +12 SP au backlog initial pour la spec AST partagée, le port TS, et le harness de golden tests.
  • Drift potentiel entre Kotlin et TS si on néglige les golden tests — risque mitigé par CI bloquante.
  • Pas de boucles ni sous-formulaires dynamiques au MVP — délibérément exclu pour ne pas exploser la complexité de l’AST. Reporté V1 (cf ADR-007 décision secondaire).
  • Versioning SemVerMAJOR.MINOR.PATCH. MAJOR = breaking change (champ supprimé / renommé / type changé). MINOR = ajout compatible (nouveau champ optionnel). PATCH = libellé / hint / branding. Le SDK Web respecte un constraint ^MAJOR.MINOR côté tenant.
  • Immuabilité après publish — une version publiée ne peut plus être modifiée. Pour corriger, créer la version suivante. La version is_active=true peut être basculée mais jamais éditée. (Cf ADR-002, retention WORM.)
  • Préchauffage — au publish, le serveur compile le FormDefinition JSON vers une représentation pré-mâchée (catalogues d’enums résolus, regex compilées, expressions normalisées) et la met en cache CDN. Le SDK Web tire la version compilée pour économiser le parsing initial.
  • Submission storage — les valeurs collectées sont stockées en JSONB, chiffrées par enveloppe avec une clé tenant (cf ADR-002 multi-tenant). Le formVersionId est figé sur la Submission pour assurer la reproductibilité même si la définition évolue après.
  • Évaluation lazy des fichiers / biométrie — le runtime évalue les règles sur les valeurs scalaires uniquement. Les uploads de fichier et la biométrie sont des “side effects” déclenchés par le SDK Web après que la step soit visible et complète, ils ne participent pas à l’AST.
  • i18n par clé centralisée — pas de strings inline dans les règles. block(message = i18n("error.minor.notEligible")) exige que la clé existe dans les 3 langues. Validation publish casse sinon.
  • DSL extensions futures — ajouter un opérateur ou une action (ex : prefill_async) exige un nouvel ADR. Évite la dérive incrémentale du langage.
OptionPourquoi écartée
YAML au lieu de JSON canoniqueMulti-ligne fragile, parser idiosyncratiques (anchors, tags), diff bruité. JSON canonique est aujourd’hui standard de fait pour les artefacts versionnés signés (cf JOSE, COSE).
BPMN / CMMNSur-puissant : nous n’avons pas de processus longs avec parallélisme, juste un wizard linéaire avec règles de visibilité. Public banque BCT lit difficilement BPMN. Outils éditeurs lourds.
Évaluateur côté serveur uniquement (round-trip à chaque step)Tue la latence sur des connexions 3G MENA. Augmente la surface d’attaque (chaque step = un endpoint authentifié). Casse l’expérience offline-resilient (déconnexion 30 s acceptable aujourd’hui via IndexedDB cache).
JSONForms / Formik schemaÉcosystème React mature mais : pas de support RTL arabe natif, pas de sémantique CPS, runtime non isomorphe pour designer Kotlin. Surcoût d’adaptation > coût build interne.
DSL custom interpreted (sans JVM compile)Supprime les invariants enforced à la compilation. Reproduit les pièges Risk Engine (champs fantômes, règles orphelines) que l’on a explicitement résolus avec ADR-026.
Form Designer tiers OEM (Form.io, Formspree)Cf ADR-007 — intégration profonde KYC (OCR routing, liveness, signature, CPS) impossible sans fork lourd.
Single runtime Kotlin/JS (Kotlin Multiplatform)Tentant mais alourdit le SDK Web (~140 KB minimum runtime KMP), augmente la surface debugging client, coûte au temps de cold start du parcours KYC. Préférons un port TS manuel + golden tests.
  • Démo S04 : DSL Kotlin opérationnel, JSON canonique publishable, validateur enforcing CPS + i18n + consent, golden tests verts.
  • Pilote S08 : SDK Web embarqué chez 1 tenant pilote tunisien, 50 règles évaluées en < 5 ms p99, drift Kotlin/TS = 0 sur 80 cas du corpus golden.
  • Revue M+6 : indicateurs — % de formulaires publiés sans warning publish (objectif ≥ 95 %), nombre de bug reports liés à un drift d’évaluation (objectif 0), taille SDK Web prod (objectif < 60 KB gzip).
  • Trigger nouvel ADR : si on doit introduire des boucles, des sous-formulaires dynamiques, ou un évaluateur asynchrone (lookup AML inline), revoir l’AST entièrement.

ADR-028 — Pipeline biométrique : orchestration capture + MRZ + liveness + face match (build interne + fallback commercial)

Section intitulée « ADR-028 — Pipeline biométrique : orchestration capture + MRZ + liveness + face match (build interne + fallback commercial) »

Statut : Accepté Date : 2026-04-26

ADR-003 a fixé la stratégie OCR (interne + fallback). Mais l’OCR n’est qu’une brique d’un parcours d’identification documentaire et biométrique complet. Le KYC moderne (FATF Reco. 10 et 11, NIST SP 800-63A IAL2/IAL3, eIDAS LoA Substantial) exige typiquement :

  1. Capture qualifiée d’un document d’identité (recto + verso si CNI, RFID si biométrique)
  2. OCR des champs visibles (déjà couvert par ADR-003)
  3. MRZ — extraction et vérification de la zone de lecture mécanique (ICAO 9303) avec ses 5 chiffres de contrôle
  4. Authenticité du document — détection de fraude (templates, patterns, hologrammes, MRZ vs OCR)
  5. Capture selfie + liveness detection (anti-spoof : photo, vidéo replay, masque 2D/3D, deepfake)
  6. Face match entre la photo du document et le selfie

Aujourd’hui VitaKYC n’a pas de spec engineering pour les étapes 3, 4, 5, 6. Sans elles, le parcours KYC se réduit à du remplissage de formulaire + upload de photo, ce qui ne tient pas l’IAL2 ni la BCT (qui exige depuis 2024 une vérification biométrique active pour l’onboarding bancaire à distance).

Trois forces en tension :

  • Conformité : iBeta PAD level 2 minimum pour le liveness en banque, certification DC/DCV (Document Authentication and Verification) attendue par les fournisseurs commerciaux.
  • Coût marginal : un appel commercial de bout en bout (Onfido, Sumsub, Veriff, Regula, Jumio) coûte 1,50–4,50 USD par parcours. Sur 10 000 onboardings/mois cela cumule 15 000–45 000 USD.
  • On-prem / air-gap : aucune des sondes commerciales ne fonctionne dans un cluster banque sans accès Internet sortant.

Architecture orchestrée par un service bio-svc qui coordonne 4 sous-services (ocr-svc déjà ADR-003, mrz-svc, liveness-svc, face-match-svc) avec stratégie hybride interne par défaut + fallback commercial routable par tenant sur les composants où l’écart de qualité avec le commercial reste matériel (liveness, face match, document authenticity). Le MRZ est 100 % interne (standard normalisé ICAO 9303, pas de différenciateur commercial).

┌──────────────┐
capture │ bio-svc │ orchestrateur
doc + selfie ─▶│ (workflow) │ (Temporal — cf ADR-001)
└──────┬───────┘
┌────────┬───────┼───────┬────────┐
▼ ▼ ▼ ▼ ▼
┌───────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐
│ ocr │ │ mrz │ │ doc │ │ live │ │ face- │
│ -svc │ │ -svc │ │ -auth│ │ -ness│ │ match │
│(ADR-3)│ │ │ │ -svc │ │ -svc │ │ -svc │
└───────┘ └──────┘ └──────┘ └──────┘ └──────────┘
│ │ │
▼ ▼ ▼
fallback commercial routable :
Regula / Onfido / Sumsub / Jumio

Choix de fond par sous-service :

ServiceBuild interne MVPFallback commercialRoutage
ocr-svcTesseract 5 + ONNX fine-tuné MENAGoogle Vision / AWS Textract / Mistral OCRseuil de confiance (cf ADR-003)
mrz-svcparseur ICAO 9303 (TD1, TD2, TD3) en Kotlin puraucun (interne suffit)100 % interne
doc-auth-svctemplate matching + heuristiques (V1)Regula Document Reader SDK / Onfido document checkfeature flag tenant — par défaut commercial au MVP
liveness-svcpassive analysis (qualité, motion, blink) en MVPOnfido Motion / Sumsub Liveness / Veriff (iBeta L2)feature flag tenant — commercial recommandé MVP
face-match-svcInsightFace ONNX (ArcFace) — score cosineOnfido / Sumsub / Regula Face SDKseuil score interne ; fallback si score ambigu
  • MRZ 100 % interne : norme ICAO 9303 publique, parseur de < 500 LOC, testable hors-ligne sur un corpus public (test vectors ICAO + IATA), zéro dépendance commerciale. Aucun fournisseur n’apporte de différenciation ici.
  • Document authenticity : c’est le maillon le plus difficile à internaliser (templates des centaines de documents par pays, hologrammes, patterns, OVI). L’écart de qualité avec Regula/Onfido reste matériel pendant 18–24 mois → on accepte le coût commercial au MVP, on internalise progressivement.
  • Liveness : iBeta L2 est nécessaire pour passer la due-diligence des banques tunisiennes et françaises. Internaliser une sonde certifiée iBeta L2 demande 8–12 mois et un coût d’audit de 60–80 K USD. Pas faisable au MVP — fallback commercial obligatoire.
  • Face match interne : InsightFace ArcFace ONNX donne une AUC > 0.99 sur LFW et CFP-FP (publics et reproductibles). Le coût marginal d’un appel commercial face match (~0,15 USD) ne se justifie pas si on a déjà un score interne fiable. On garde le commercial pour les cas ambigus.
  • Air-gap : mrz-svc, face-match-svc, et ocr-svc interne fonctionnent sans Internet. doc-auth-svc Regula a un mode SDK on-prem (binaire embarqué). liveness-svc interne PAD passif fonctionne hors-ligne. Le tenant air-gap a un parcours complet sans appel sortant.
  • Orchestration Temporal (cf ADR-001) : la pipeline biométrique est un workflow long (capture → OCR → MRZ → cross-check → liveness → face match → décision) avec retries, timeouts, compensations. Temporal est déjà la décision pour ce type de besoin.

Positives

  • Conformité IAL2 et BCT 2024 atteignable au MVP avec composition contrôlée (liveness commercial + face match interne).
  • Coût marginal ~0,40–0,80 USD par parcours médian (mix interne/commercial) au lieu de 1,50–4,50 USD si full commercial.
  • Air-gap couvert (mode dégradé documenté : pas de doc-auth commercial → score doc baissé → règle de risque tenant compensatoire).
  • MRZ interne testable et auditable (POC poc-mrz-parser livré).

Négatives

  • 5 services à orchestrer + 4 adapters commerciaux à maintenir (Regula, Onfido, Sumsub, Jumio).
  • Complexité de monitoring (métriques par sous-service + drift face-match interne vs commercial).
  • Coût initial des SDK Regula on-prem (licence ~12 K USD/an pour le MVP, bandeable).
  • Capture obligatoirement double : recto + verso sur CNI/permis ; recto + MRZ sur passeports ; selfie obligatoire pour tout segment client. Le Form Designer (ADR-027) refuse la publication d’un form KYC sans field type DOC_KYC + SELFIE (sauf segment “corporate documents only” qui exige une autre piste — cf cas Société).
  • Cross-check OCR ↔ MRZ : si l’OCR du nom et la MRZ divergent sur le nom de famille (distance de Levenshtein > 2), le case est flagué manual_review_required et l’agent compliance tranche.
  • Score face match consolidé : score_final = w_intern * score_arcface + w_commercial * score_commercial avec poids configurables tenant (défaut 0.6/0.4). Si commercial désactivé, w_intern = 1.0.
  • Liveness mode dégradé : si liveness-svc (commercial) ne répond pas après timeout 12 s + retry, le case bascule en manual_review_required plutôt qu’en rejet automatique. Métrique alertée.
  • Stockage biométrique : photos de visage stockées chiffrées (KMS Vault per-tenant, AES-256-GCM), retention 5 ans (cf RGPD art. 9 traitement biométrique + obligations BCT 10 ans pour les pièces justificatives — la photo n’est pas la pièce justificative, mais sa preuve d’extraction l’est). Suppression automatique au terme.
  • Aucun template biométrique transmis externe : seules les scores (face match score, liveness score) sont propagés vers risk-matrix ; jamais les vecteurs InsightFace (privacy). Cf DPIA.
  • Modèles ONNX versionnés : chaque modèle (face_match_v1, liveness_passive_v1) a un model card YAML inline + un hash SHA-256 + une version SemVer. Toute mise à jour passe par un workflow de revue.
  • Pas de PAD niveau 3 au MVP : iBeta PAD L3 (résistance aux masques 3D résine) reste rare en banque détail. Reporté V2.
OptionPourquoi écartée
100 % commercial bout-en-bout (Onfido / Sumsub end-to-end)Coût marginal 1,50–4,50 USD/parcours — incompatible avec la promesse pricing VitaKYC (35–60 % moins cher que les leaders). Air-gap impossible.
100 % interne au MVPiBeta L2 pas atteignable en 6 mois sans budget audit. Document authenticity demande un dataset privé massif (templates par pays). Risque de non-conformité bancaire.
Build sans MRZ (juste OCR)MRZ est la source de vérité d’un passeport — l’OCR du nom peut être bruité, la MRZ est déterministe (ICAO 9303 + 5 checksums). Skipper la MRZ = perdre la vérification d’intégrité du document.
Liveness uniquement passif au MVPPassive seul ne passe pas iBeta L2. Banques TN demandent une preuve active. Compromis : passif comme pré-filtre + commercial certifié pour le verdict final.
Face match délégué au document RFID (ICAO BAC/PACE puis lecture du DG2)Élégant mais demande un téléphone NFC ET un passeport biométrique. Couvre < 30 % du parc TN/MENA. Reporté V2 en option.
Centraliser tout dans bio-svc monolithiqueTue l’évolutivité — chaque sous-service a un rythme de release et un type d’erreur différent (modèles ML, intégration commerciale, parseur déterministe). Garder en services séparés permet aussi le scale différencié.
MetricMVP cibleV2 cible
Latence p95 pipeline complète≤ 8 s≤ 5 s
Coût marginal médian par parcours (mix interne/commercial)≤ 0,80 USD≤ 0,30 USD
Taux de fallback commercial face match≤ 25 %≤ 10 %
Faux rejets (TAR @ FAR 0.01 %) face match≥ 95 %≥ 98 %
iBeta PAD L2 sur livenesscertifié via fournisseurinterne en cours
Disponibilité bio-svc99,5 %99,9 %
  • Démo S05 : pipeline E2E sur 1 doc TN + selfie, score consolidé, latence < 12 s.
  • Pilote S10 : 1 banque TN avec 200 onboardings réels, taux fallback mesuré.
  • Revue M+6 : décision sur internalisation liveness-svc (audit iBeta budgétisable selon revenue).
  • Trigger nouvel ADR : si BCT impose PAD L3, ou si on internalise complètement le liveness.

ADR-029 — Case Management : workflow agent compliance, file d’attente skill-based, SLA escaladable, audit trail append-only

Section intitulée « ADR-029 — Case Management : workflow agent compliance, file d’attente skill-based, SLA escaladable, audit trail append-only »

Statut : Accepté Date : 2026-04-27

Les ADR précédents (ADR-025 Risk Matrix, ADR-028 pipeline biométrique) produisent des verdicts qui aboutissent souvent à MANUAL_REVIEW_REQUIRED. À ce stade un agent compliance doit prendre la main, regarder le case, demander des compléments éventuels, décider, escalader si nécessaire. Sans un module Case Management proprement spécifié :

  • Les agents traitent les cases via des outils ad-hoc (mail, Excel) → audit BCT impossible.
  • Pas de SLA mesurable → la banque ne peut pas s’engager auprès du client final (“réponse sous 4 h”).
  • Pas de routage par compétence → un L1 récent reçoit une décision PEP complexe → erreurs.
  • Pas de trace append-only des actions → la BCT ne peut pas reproduire la décision.
  • Pas d’escalade automatique → un case en stand-by 48 h passe sous le radar.

Trois forces en tension :

  1. Conformité / audit : la BCT (Circulaire 2017-08 + LCB-FT 2015-26) impose une traçabilité complète des décisions de risque, irrévocable, signée et conservée 10 ans.
  2. Productivité agent : un agent doit traiter 30–60 cases/jour. Une UX mal foutue (clics multiples, attentes serveur) coûte 30 % de capacité.
  3. Souplesse de configuration tenant : chaque banque a ses skill-sets, ses seuils SLA, ses règles d’escalade. Un workflow rigide ne s’adapte pas.

Architecture case-mgmt-svc centrée sur un état machine déterministe, un router skill-based scoré, un moteur SLA dérivé de Temporal, et un audit trail append-only signé. Toutes les décisions structurantes sont configurables par tenant via une CasePolicy (DSL Kotlin réutilisant la grammaire ADR-004).

Architecture en couches :

┌────────────────────────────┐
triggers d'arrivée │ case-mgmt-svc │
───────────────▶ │ - state machine │
bio.verdict │ - router skill-based │
risk.evaluation │ - SLA engine (Temporal) │
aml.alert │ - 4-eyes engine │
│ - audit trail append-only │
└──────────────┬─────────────┘
┌────────────────────────┼────────────────────────┐
▼ ▼ ▼
back-office UI agents queue API Kafka case.decided
compliance officers (poll, claim, decide) risk-matrix-svc
+ workflow client

5 décisions structurantes :

  1. State machine déterministe — un case parcourt les états NEW → ASSIGNED → IN_REVIEW → DECISION_PENDING → APPROVED|REJECTED|ESCALATED → CLOSED. Aucune transition implicite, chaque transition est un event signé append-only. Pas de back-states (impossible de retourner d’APPROVED à IN_REVIEW — il faut ouvrir un nouveau case case_reopen lié).

  2. Routing skill-based scoré — chaque agent a un profil {skills: [PEP, FATCA, CRYPTO, RETAIL_TN, ...], seniority: L1|L2|MLRO, currentLoad: int}. Chaque case a un vecteur {requiredSkills: [...], priority: LOW|STANDARD|HIGH|URGENT}. Le router calcule un score d’adéquation et assigne au top-N (avec affectation équitable round-robin pour départager).

  3. SLA dérivé Temporal — chaque case démarre un workflow Temporal CaseSlaWorkflow qui dort jusqu’au deadline puis vérifie ; si toujours en attente, déclenche escalade. SLA configurable par (priority × tenant). Échéance ré-armée après chaque transition d’état.

  4. 4-eyes principle — pour les cases “sensibles” (montant > seuil tenant, PEP, ESCALATED par L1), une décision exige deux signatures distinctes (deux agents différents avec rôles différents). Engine 4-eyes intégré, pas un add-on.

  5. Audit trail append-only signé — chaque event (case.created, case.assigned, case.commented, case.documentAttached, case.decided, case.escalated) est sérialisé canonique JSON, hashé SHA-256, signé Ed25519 par l’acteur, puis appendé dans une table case_event immutable (pas d’UPDATE/DELETE au niveau Postgres). Conservation 10 ans WORM.

  • State machine explicite plutôt que workflow flexible : audit BCT exige la reproductibilité — un état suffit à raconter ce qui s’est passé. Les transitions sont énumérables et testables.
  • Routing scoré plutôt que premier disponible : un L1 qui voit son premier PEP fait 3× plus d’erreurs qu’un L2. Le coût d’erreur en compliance est asymétrique (faux négatif → amende BCT 100 K TND ; faux positif → friction client). On doit privilégier la qualité d’assignation.
  • SLA via Temporal plutôt que cron polling : Temporal garantit l’exactly-once des escalades même en cas de redémarrage du service. Le pattern workflow.sleep(deadline) est natif et testable.
  • 4-eyes intégré plutôt que feature flag externe : c’est une obligation BCT pour les transactions importantes (Circulaire 2018-07). Le mettre en option externe = risque d’oubli côté tenant.
  • Audit append-only signé plutôt que log applicatif : un audit log non signé peut être édité par un admin DBA. La signature Ed25519 par l’acteur prouve qu’à l’instant T, l’agent X a fait l’action Y avec le hash Z. Inviolable même par un opérateur interne.

Positives

  • Conformité BCT démontrable : un audit demande “comment a été décidé ce case ?” → on rejoue les events signés.
  • SLA mesurables : KPI dashboard tenant + alertes opérationnelles si dépassement.
  • Productivité agent : routing intelligent réduit le temps de revue moyen de 25–35 % (mesuré chez compétiteurs).
  • 4-eyes prouvable : impossible de déployer un cas sensible avec une seule signature.
  • Multi-tenant strict : RLS Postgres + signature par tenant key.

Négatives

  • Coût initial +22 SP au backlog MVP (state machine + router + SLA workflow + 4-eyes + audit + UI back-office).
  • Complexité de tests (combinatoire d’états × priorités × skills × SLA × 4-eyes).
  • Onboarding agent : il faut former chaque nouvel agent au workflow et aux raccourcis UI (~4 h de formation).
  • Tooling de migration de profils agents (skills, seniority) entre versions.
  • Cases liés — un case peut être lié à un autre (linkedTo: caseId) avec relation typée (reopen_of, escalated_to, merged_into, superseded_by). Le graphe de cases est navigable via API.
  • Priorité dynamique — la priorité peut être bumpée pendant le cycle de vie (ex : nouveau renseignement reçu, montant transaction reclassé). Bump tracé dans audit trail.
  • Commentaires + pièces jointes — internes (visibles agent only) vs externes (visibles client final). Distinction enforcée par UI + API.
  • Auto-décision après timeout — pour des cases LOW non décidés en 7 jours : auto-rejected avec motif “timeout” et notification client. Configurable par tenant (défaut OFF).
  • Reassignment — un agent peut renoncer à un case → renvoyé au router. Limité à 2× par case pour éviter le ping-pong.
  • Pause / reprise — un case peut être mis en pause (en attente document client) sans consommer le SLA. Reprise déclenche un re-routage si l’agent initial est indisponible.
  • Search full-text — recherche sur subjectName, caseId, comments via OpenSearch (multilingue ICU, cf ADR-013 backlog).
  • Bulk operations — un MLRO peut traiter en masse (ex : “rejet en lot pour fraude détectée”) avec audit individuel par case.
  • Notifications — par email (compliance officer) + Kafka (autres systèmes). Pas de SMS dans MVP.
  • Retention — 10 ans WORM côté events ; 7 ans côté commentaires + pièces jointes (sauf si contrôlable par BCT). Suppression automatisée passé le terme.
OptionPourquoi écartée
Outil tiers OEM (ServiceNow, Salesforce Compliance Cloud)Coût licence > 100 K USD/an, pas de RLS multi-tenant natif, pas d’audit trail signé crypto, intégration Kafka payante. Sur 5 ans, coût > build interne.
State machine flexible (ex BPMN éditable par admin)Tue l’audit : un admin pourrait modifier le workflow en cours. Audit BCT impossible.
Routing FIFO simpleÉlimine la sémantique skill — tout L1 reçoit tout case. Performance qualité < 60 % du routing scoré.
SLA via cron pollingRisque de double-escalade si le cron tourne pendant un redémarrage. Pas natif testable.
Audit log non signéInsuffisant pour BCT — un opérateur interne pourrait éditer.
4-eyes en plugin externeRisque d’oubli côté tenant ; dépendance externe. Préférable comme primitive intégrée.
Pas de bulk operationsUn MLRO doit pouvoir traiter en lot lors de découverte de fraude organisée. Sans bulk, il quitte l’outil.
MetricMVP cibleV2 cible
Latence assignment p95≤ 200 ms≤ 100 ms
Précision routing skill-based (top-1 = bonne classe agent)≥ 80 %≥ 92 %
Taux SLA respecté (priorité STANDARD)≥ 90 %≥ 95 %
Taux 4-eyes appliqué quand requis100 % (enforcé serveur)100 %
Disponibilité case-mgmt-svc99,5 %99,9 %
Tests automatisés couvrant state machine100 % des transitionsidem
  • Démo S05 : state machine + router fonctionnels avec 1 tenant pilote, 50 cases simulés.
  • Pilote S10 : 1 banque TN, 200 cases réels traités par 5 agents, KPI mesurés.
  • Revue M+6 : précision routing, SLA respect, satisfaction agent (NPS interne).
  • Trigger nouvel ADR : si on doit ajouter une dimension (ex : multi-team avec hiérarchie complexe), revoir le router.
  • ADR-001 — Temporal pour SLA workflows
  • ADR-002 — multi-tenant RLS
  • ADR-004 — DSL pour CasePolicy
  • ADR-025 — déclencheur via verdict HIGH
  • ADR-028 — déclencheur via MANUAL_REVIEW_REQUIRED
  • BCT Circulaire 2017-08 (LCB-FT) + 2018-07 (4-eyes), Loi 2015-26
  • Page engineering : Case Management
  • POC : poc-case-mgmt

ADR-030 — Sanctions screening : OpenSearch unique packagé + re-ranker Kotlin + RCA dénormalisé ≤ 2 sauts + audit log signé

Section intitulée « ADR-030 — Sanctions screening : OpenSearch unique packagé + re-ranker Kotlin + RCA dénormalisé ≤ 2 sauts + audit log signé »

Statut : Accepté Date : 2026-04-27

ADR-006 a posé la stratégie de gestion des listes AML (pipeline incrémental + air-gap). Manque la spec engineering du moteur de matching lui-même. Sans elle :

  • pas de spec d’algorithme reproductible auditable BCT
  • pas de réponse claire sur la stratégie d’index (broad search) vs algorithmique pure
  • pas de gestion des noms MENA (translittération arabe, variantes phonétiques) qui sont la spécificité commerciale de VitaKYC vs concurrents EU/US
  • pas de gestion des associations RCA (Relatives & Close Associates) qui représentent ~40 % de la valeur d’une licence Dow Jones

Trois forces en tension :

  1. Précision audit BCT : un screening positif doit être reproductible 10 ans plus tard. La BCT exige (version_liste, query, top-N candidats, scores) archivés WORM.
  2. Performance live : le screening tourne pendant l’onboarding → cible ≤ 200 ms p95 end-to-end pour ne pas dégrader le parcours client.
  3. Couverture noms MENA : un même nom arabe peut s’écrire Mohammed/Mohamed/Muhammad/Mhammed/محمد. Sans translittération + matching phonétique, on rate ~25 % des matches dans les pays arabes.

Volumes typiques :

  • Listes publiques (OFAC SDN + UN + EU CFSP + UK OFSI + World Bank + agrégat OpenSanctions) : ~35 K entrées uniques
  • Dow Jones Watchlist (option payante client) : 3,8 M entités + 8 M edges d’associations (RCA, business, address)

Architecture OpenSearch unique comme moteur de broad search, packagé dans la distribution VitaKYC (pas une charge d’ops tenant), avec re-ranker Kotlin déterministe comme cœur d’audit, dénormalisation RCA flattened à ≤ 2 sauts, et audit log signé Ed25519 comme source de vérité reproductible.

5 décisions structurantes :

  1. OpenSearch comme broad search unique — un seul moteur pour toutes les listes (publiques 35 K + Dow Jones 3,8 M). Multi-fields ICU + phonetic + n-gram + raw. Query dis_max cross-fields + multi_match. Score natif uniquement comme rappel — pas comme verdict.

  2. Re-ranker Kotlin pur déterministe — c’est lui qui produit le score final. Algorithmes : Jaro-Winkler (nom), Soundex / Beider-Morse (phonétique), Levenshtein (alias), DOB cross-check, nationality cross-check, AKA unfolding (translittération arabe). Reproductible bit-à-bit, testable hors infra, indépendant du backend OpenSearch.

  3. RCA dénormalisé à ≤ 2 sauts — au moment de l’ETL Dow Jones, on pré-calcule pour chaque entité X la liste flattened_associated_sanctioned[] (toutes les entités sanctionnées atteignables ≤ 2 sauts). Recalculé au reindex hebdo. Coût stockage ~1.5× l’index brut. Couvre 90 % des cas RCA banque (frère sanctionné, UBO indirect). > 2 sauts = nouvel ADR avec graph DB.

  4. Audit log signé append-only — chaque screening émet un event sanctions.screening.completed avec (queryNormalisée, listVersion, topNCandidats, scoresReRanker, rcaPath, decision, signatureEd25519). Source de vérité auditable BCT, indépendante du moteur d’index. Si demain on switch ES→OS→autre, le log reste valide.

  5. Packaging à 2 modesembedded (container Docker single-node OpenSearch + tar.gz, 4-8 GB RAM, géré par VitaKYC, pour tenants light) ou external (cluster 3 nœuds existant chez tenant tier-1). Même image VitaKYC, déclenché par feature flag tenant.

  • OpenSearch unique plutôt que SQLite + OpenSearch hybride : cohérence d’exploitation, embauche/formation simplifiée, monitoring uniforme. Le sur-dimensionnement pour 35 K entrées est marginal vs la complexité de maintenir 2 backends.
  • OpenSearch plutôt qu’Elasticsearch : licence Apache 2.0 (vs SSPL/Elastic License), distribuable on-prem sans clause commerciale.
  • Re-ranker Kotlin central : un upgrade ICU plugin OpenSearch ne doit jamais changer un score de match passé. Le score final est calculé par le re-ranker, déterministe, testable en CI sur un corpus golden.
  • Dénormalisation RCA : Postgres recursive CTE est plus naturel mais ajoute un backend. Flattening dans OpenSearch coûte du stockage (~50 %) mais simplifie l’archi. 2 sauts couvrent le besoin réel banque.
  • Packaging embedded : lève la barrière “trop lourd pour petit tenant on-prem”. GitLab, Sourcegraph, Elastic Enterprise Search font pareil. Le tenant n’a pas à connaître OpenSearch.
  • Audit log signé : pattern industrie (ComplyAdvantage, Refinitiv) — on archive les matches, pas les moteurs.

Positives

  • Stack uniforme (1 moteur, 1 monitoring, 1 backup, 1 profil ops).
  • Couverture MENA propre (ICU translittération + phonetic Beider-Morse natif).
  • Reproductibilité audit BCT via audit log signé indépendant du moteur.
  • Performance attendue : p95 broad ≤ 30 ms, re-ranker ≤ 50 ms, total ≤ 200 ms.
  • Distribution simple : 1 image Docker, 1 tar.gz pour air-gap, 1 documentation packaging.

Négatives

  • RAM minimum 4-8 GB pour container OpenSearch même tenant léger (vs 0 ops SQLite).
  • Upgrade OpenSearch = re-test du re-ranker + re-index complet → fenêtre de maintenance ~1 h.
  • RCA limité à 2 sauts au MVP — un cas marginal banque “fund managé par société dont l’UBO ultime est PEP à 4 sauts” reste manuel.
  • Coût ressources cluster Dow Jones : 3-node 16 GB chacun → ~60-100 USD/mois/tenant en cloud, plus en on-prem.
  • Listes publiques au MVP : OFAC SDN, OFAC Consolidated, UN Consolidated, EU CFSP, UK OFSI, World Bank Debarred, agrégat OpenSanctions. URLs canoniques documentées dans la spec engineering. Pipeline ETL : XML/JSON → normalisation FtM (Follow the Money schema OpenSanctions) → bulk index.
  • Dow Jones : adapter optionnel via feature flag tenant. Schéma DJ Watchlist XML/JSON — détails tenant-spécifiques (format, identifiants, RCA edges) traités à l’activation.
  • Threshold par typologie de hit : OFAC SDN 0.92 (strict) / Dow Jones PEP 0.85 / RCA 1-hop 0.80 / RCA 2-hop 0.70 / Adverse Media manual 0.75. Configurable par tenant.
  • Cadence sync : full reindex hebdomadaire dimanche 02:00 UTC ; delta quotidien à 06:00 UTC ; force-refresh on-demand admin.
  • Versioning des listes : chaque ETL emet listVersion = sha256(sorted(entries)) pour reproductibilité audit. Snapshots OpenSearch versionnés dans MinIO sur 10 ans (~10 GB/an pour DJ, négligeable pour publiques).
  • Caching résultats : 24 h sur (queryNormalisée, listVersion). Cache invalidé au reindex.
  • Privacy MENA : noms arabes stockés en NFC + translittérés (icu_folded), version originale conservée pour affichage. Pas de hashing PII (sinon impossible à matcher).
  • Cross-tenant isolation : index par tenant (sanctions_TN-BANQUEX_v42). Pas de partage. Un tenant qui bascule de fournisseur (ex DJ→ComplyAdvantage) re-index from scratch.
  • OFAC delisting : si une entité est retirée d’une liste, l’audit log conserve la trace mais l’index courant l’exclut. Re-screening d’un cas historique réutilise le snapshot de l’époque.
OptionPourquoi écartée
SQLite FTS5 pour listes publiques + OpenSearch pour DJHybride coûte 2 backends à maintenir, double tooling, double monitoring. Sur 35 K entrées le sur-dimensionnement OpenSearch est négligeable.
Elasticsearch (vs OpenSearch)Licence SSPL / Elastic License — risque commercial pour distribution on-prem multi-clients. OpenSearch fork Apache 2.0 résout.
Lucene custom KotlinRéinvente l’ETL, le monitoring, le scaling, l’API REST. Coût > bénéfice.
100 % SaaS commercial bout-en-bout (ComplyAdvantage, Sumsub Screening)Coût marginal 0,15-0,40 USD/screening × 10 K /mois = 1,5-4 K USD/mois minimum. Air-gap impossible. Banque tunisienne = no go.
Postgres recursive CTE pour graph 8 M edgesMarche bien (testé sur autres projets) mais ajoute un backend. La dénormalisation RCA dans OpenSearch est plus simple si on accepte ≤ 2 sauts.
Neo4j pour graph 8 M edgesSurplus pour ≤ 2 sauts. Licence GPL pour Community ou commerciale pour Enterprise. À ouvrir si > 2 sauts demandé.
Pas de re-ranker, OpenSearch score = verdictScore OpenSearch dépend des analyzers — un upgrade ICU change tous les scores rétroactivement. Casse l’audit BCT.
Algo phonetic Soundex seul (pas Beider-Morse)Soundex est anglo-saxon, faible sur arabe. Beider-Morse couvre mieux noms slaves, juifs, arabes (Daitch-Mokotoff variante optimisée).
MetricMVP cibleV2 cible
Latence broad OpenSearch p95≤ 30 ms≤ 15 ms
Latence re-ranker p95 (top-50)≤ 50 ms≤ 25 ms
Latence pipeline complète p95≤ 200 ms≤ 100 ms
Précision @ threshold OFAC 0.92≥ 99 %≥ 99,5 %
Rappel @ threshold OFAC 0.92≥ 95 %≥ 98 %
Faux positifs / vrais positifs≤ 5:1≤ 2:1
Reindex full publiques (35 K)≤ 1 min≤ 30 s
Reindex full DJ (3,8 M)≤ 60 min≤ 30 min
Disponibilité screening service99,5 %99,9 %
  • Démo S05 : screening sur OFAC SDN public, re-ranker Kotlin opérationnel, audit log signé, p95 ≤ 200 ms.
  • Pilote S10 : 1 tenant TN, 200 onboardings, 0 faux négatif sur jeu de test BCT.
  • Revue M+6 : précision/rappel mesurés réels, ratio FP/TP, calibration thresholds par tenant.
  • Trigger nouvel ADR : > 2 sauts RCA demandé, ou un fournisseur exotique (ex Refinitiv WorldCheck) avec format incompatible OpenSearch ETL.

ADR-031 — Transaction Monitoring streaming : Kafka Streams + rules DSL Kotlin + sliding windows + alert dedup + outbox

Section intitulée « ADR-031 — Transaction Monitoring streaming : Kafka Streams + rules DSL Kotlin + sliding windows + alert dedup + outbox »

Statut : Accepté Date : 2026-04-27

La page AML Transaction Monitoring a posé l’architecture haut-niveau (formats d’ingestion, intégration core banking, typologies de règles). Manque la spec engine streaming — comment on évalue les règles métier en temps réel sur le flux de transactions, comment on gère les fenêtres glissantes (structuring, smurfing, velocity), comment on déduplique les alertes, comment on garantit l’exactly-once entre détection et émission de l’alerte.

L’enjeu est central pour la conformité BCT (Circulaire 2017-08 §IV exige détection de transactions atypiques en temps réel pour les banques) et pour la promesse commerciale VitaKYC (vs ACI Worldwide, NICE Actimize, Hawk AI).

Trois forces en tension :

  1. Latence faible : alerter dans les secondes après une transaction suspecte (pas dans l’heure batch). Cible : p95 ≤ 500 ms entre arrivée Kafka et émission alerte.
  2. Détection multi-fenêtres : montant cumulé > 50 000 TND sur 24 h ou ≥ 10 transactions structurées de < 10 000 TND sur 7 j. Demande state stores persistants par client + fenêtres glissantes parallèles.
  3. Reproductibilité audit BCT : le compliance officer doit pouvoir re-jouer une alerte dans 5 ans avec les exactes mêmes règles, le même état du compte client, et obtenir le même verdict.

Volumétrie typique tenant tier-1 :

  • 50 000 – 500 000 transactions/jour
  • 100 – 1 000 alertes/jour
  • 5 000 – 50 000 comptes actifs
  • Pic journalier : x3 le jour de paye / fin de mois

Architecture Kafka Streams comme moteur de streaming + rules DSL Kotlin qui réutilise la grammaire de prédicats de ADR-004/ADR-025 + sliding windows hopping + alert deduplication par hash signature + outbox pattern vers Kafka topic aml.alert.published.

6 décisions structurantes :

  1. Kafka Streams plutôt que Flink — réutilise le cluster Kafka déjà en place (cf ADR-001 Temporal), profil ops simple (JVM lib embeddable dans tx-monitoring-svc, pas de cluster séparé), maturité éprouvée en banque (ING, Capital One).

  2. Rules DSL Kotlin déclaratif — chaque règle est un objet immutable composé de : WindowSpec, Predicate AST (réutilise grammaire ADR-004), AggregateExpr (count/sum/avg/distinct), Threshold, AlertSeverity, AlertTemplate. Sérialisable JSON canonique pour audit + signature dual-control.

  3. Sliding windows hopping — chaque règle déclare son WindowSpec (size, step, gracePeriod). Les windows hopping permettent un compromis perf/précision (ex : size=24h, step=1h, grace=5min = 24 windows actives par compte). Tumbling windows pour les règles non-continues (ex daily summary).

  4. State stores RocksDB local — chaque pod tx-monitoring-svc maintient son state store local (compteurs, sommes, distinct counts par compte) backé sur un changelog Kafka topic compacté. Resilient aux crashes (rejoue le changelog au démarrage).

  5. Alert dedup par signature SHA-256alertId = sha256(ruleId + accountId + windowStart + sortedTriggeringTxIds). Si le même alertId est émis 2× (re-processing après crash, late-arriving event), dedup automatique côté outbox writer. Garantit exactly-once effective malgré at-least-once Kafka.

  6. Outbox pattern Postgres → Kafka — l’alerte est d’abord persistée en Postgres alert_outbox dans la même transaction que la maj de l’état. Un sweeper indépendant pousse vers Kafka aml.alert.published. Pattern éprouvé pour exactly-once trans-frontière DB ↔ broker.

  • Kafka Streams : on a déjà Kafka (events bio.verdict, risk.evaluation, case.*, sanctions.*). Réutiliser le même broker = 1 seul opérationnel. Flink demande un cluster séparé (JobManager + TaskManager + checkpoint storage) — overkill pour notre profil banque MENA.
  • Rules DSL : un nouveau langage métier serait dramatique. Réutiliser la grammaire ADR-004/025 (Predicate.And/Or/Not, opérateurs, field()/cps()) → un compliance officer qui maîtrise Risk Matrix maîtrise Transaction Monitoring.
  • Sliding windows hopping : meilleur compromis pour structuring detection (granularité fine + faible cost). Tumbling raterait des cas qui chevauchent les bordures de fenêtres.
  • State stores RocksDB local : performance (lecture in-memory), resilient via changelog compacted topic. Pas besoin de Redis externe.
  • Alert dedup signature : la BCT exige reproductibilité — si on re-traite le flux, on doit obtenir les mêmes alertId. Hash déterministe sur les inputs garantit ça.
  • Outbox : pattern industrie (Spring Modulith, Debezium). Garantit que si l’alerte est en DB, elle finira sur Kafka. Si elle est sur Kafka, elle est en DB. Pas de double-write inconsistency.

Positives

  • Latence p95 ≤ 500 ms entre tx Kafka et alerte (vs heures en batch).
  • Cohérence DSL Risk Matrix ↔ TxMon ↔ Form Designer rules — courbe d’apprentissage minimale pour un compliance officer.
  • Resilient aux crashes (changelog Kafka rejoué au boot).
  • Reproductibilité audit BCT : règle versionnée + état compte snapshot + tx logs → alerte rejouable bit-à-bit.
  • Throughput horizontal scalable (partition par accountId, parallélisation par instance tx-monitoring-svc).

Négatives

  • Coût RAM : state stores RocksDB local = ~500 MB par 100 K comptes actifs. Pour banque tier-1 50 K comptes actifs = 250 MB local par pod.
  • Complexité opérationnelle : Kafka Streams demande tuning (num.stream.threads, cache.max.bytes.buffering, retention changelog), formation SRE.
  • Latence cold-start : au boot d’un pod, rejoue le changelog (~30-60 s pour 50 K comptes) avant d’être prêt à produire des alertes. Acceptable mais pas instantané.
  • Pas de Flink ML : si on veut ajouter du ML temps réel (anomaly detection sur série temporelle), Kafka Streams est limité — il faudra extraire vers Flink ou ksqlDB plus tard.
  • Topic tx.normalized : entrée du moteur, partitionné par accountId (cohérence ordering par compte). Schema Avro versionné.
  • Topic aml.alert.published : sortie. Partitionné par tenantId. Consommé par case-mgmt-svc (crée case si severity ≥ MEDIUM) et notification-svc (email compliance + dashboard live).
  • Late-arriving events : grace period 5 min par default. Au-delà, event dropé avec metric aml_late_event_dropped_total. Configurable par tenant.
  • Versioning des règles : chaque règle a un ruleVersion + signature Ed25519. Publication exige dual-control (cf ADR-029, ADR-030). Audit log aml_rule_audit append-only.
  • Shadow mode : nouvelle règle activée en SHADOW pendant 14 jours — produit alertes mais en aml.alert.shadow separate topic, pas vers case-mgmt. Comparé au prod en backtest.
  • Backtest : rejeu sur 6 mois d’historique tx avec règle candidate, comparaison FP/FN avec règle prod. Feature obligatoire avant publication.
  • Typologies de règles MVP : structuring (smurfing), velocity (vélocité), threshold (seuil simple), pattern (séquence), aggregate (somme glissante), peer-deviation (anomalie vs pairs même profession).
  • STR/SAR autogeneration : alerte severity=HIGH + agent confirme → génère draft STR goAML (cf poc-goaml-generator) pré-rempli.
  • Privacy : tx-monitoring-svc traite des PII (montant, contreparties). Chiffrement au repos (RocksDB encrypted) + tenant key segregation.
OptionPourquoi écartée
Apache FlinkCluster séparé JobManager/TaskManager, complexité ops > bénéfices à notre échelle. À reconsidérer si > 5 M tx/jour ou besoin ML streaming complexe.
ksqlDBSurcouche SQL Kafka Streams — ajoute un layer sans gain métier. Notre DSL Kotlin reste plus expressif et auditable.
Esper / Drools FusionExcellents mais commerciaux ou complexes (CEP avec syntaxe propre). Notre DSL Kotlin est plus simple à enseigner et plus auditable BCT.
Spring Cloud Stream + custom rulesPossible mais ré-invente l’orchestration sans gagner. Spring Cloud Stream ajoute de l’overhead RAM.
Batch only (Spark)Latence en heures, incompatible BCT 2017-08 §IV temps-réel.
Event Sourcing pur (Axon)Surdimensionné pour le besoin “alert sur transaction” — Event Sourcing est utile si on veut reconstruire l’état du monde, pas pour streaming alerts.
Redis Streams + custom rulesPerformance OK, mais durabilité moindre, pas de exactly-once natif, pas de changelog compacté gratuit.
MetricMVP cibleV2 cible
Latence p95 (tx Kafka → alert published)≤ 500 ms≤ 100 ms
Throughput≥ 1 000 tx/s/pod≥ 10 000 tx/s/pod
Cold-start (replay changelog)≤ 60 s≤ 20 s
Alert dedup rate≥ 99 % (zéro double)100 %
State store size par pod (50 K comptes actifs)≤ 250 MB RocksDB + ≤ 4 GB RAM≤ 200 MB
Disponibilité tx-monitoring-svc99,5 %99,9 %
Late event drop rate≤ 0,1 %≤ 0,01 %
  • Démo S05 : engine streaming opérationnel sur 4 typologies (structuring, velocity, threshold, peer), latence < 1 s mesurée.
  • Pilote S10 : 1 banque TN avec 50 000 tx/jour, 100 alertes/jour, taux FP ≤ 5 %.
  • Revue M+6 : précision/rappel mesurés, calibration thresholds par tenant, satisfaction compliance officer.
  • Trigger nouvel ADR : si on doit ajouter du ML streaming (anomaly detection autoencoder), revoir Flink. Si > 100 M tx/jour, ré-évaluer le sharding.
  • ADR-001 — Kafka déjà broker pour Temporal events
  • ADR-002 — RLS multi-tenant
  • ADR-004 — DSL Kotlin règles métier réutilisé
  • ADR-025 — grammaire de prédicats partagée
  • ADR-029 — alertes ouvrent des cases
  • ADR-030 — patterns dual-control + audit signé partagés
  • BCT Circulaire 2017-08 §IV (transactions atypiques temps réel), 2018-07 (4-eyes)
  • FATF Recommandation 20 (STR), Wolfsberg Group transaction monitoring guidance
  • Pages : AML Transaction Monitoring (haut-niveau), Streaming engine (spec deep)
  • POC : poc-aml-rules-engine

ADR-032 — Webhooks signés : HMAC-SHA256 + replay protection + outbox + retry exponentiel

Section intitulée « ADR-032 — Webhooks signés : HMAC-SHA256 + replay protection + outbox + retry exponentiel »

Statut : Accepté Date : 2026-04-27

Le cycle KYC + AML produit de nombreux events sortants vers les systèmes tenant : form.published, bio.verdict.published, risk.evaluation.published, sanctions.screening.completed, case.decided, aml.alert.published. Les tenants reçoivent ces events via webhooks HTTP POST vers leurs URLs. Sans spec dédiée :

  • pas de standard de signature (chaque tenant code son vérificateur ad-hoc avec risque d’erreur)
  • pas de protection replay (un attaquant qui intercepte un webhook peut le rejouer 1 mois plus tard)
  • pas de retry policy uniforme (chaque module ré-implémente son backoff)
  • pas de garantie at-least-once (perte de webhook si HTTP fail au moment de l’émission)
  • pas de dashboard livraison (combien de webhooks ont échoué cette semaine ? quel tenant ?)

C’est un sujet transversal (utilisé par ≥ 6 modules) qui mérite une décision unifiée.

Pattern unifié webhook-svc : émetteur central + spec de signature + protocole + outbox + dashboard, consommé par tous les modules via interface WebhookEmitter.

5 décisions structurantes :

  1. Signature HMAC-SHA256 — header X-VitaKYC-Signature: t={timestamp},v1={hmac} calculé sur ${timestamp}.${rawBody} avec une clé secrète par tenant (rotable). Format inspiré de Stripe (industrie de fait).

  2. Replay protection par timestamp + nonce — header X-VitaKYC-Timestamp: 1745000000 ; le tenant rejette si |now - timestamp| > 5 min. Nonce optionnel (X-VitaKYC-Nonce) pour idempotence côté tenant.

  3. Outbox Postgres → HTTP — chaque module appelle WebhookEmitter.queue(eventType, payload) qui insert dans webhook_outbox Postgres dans la même transaction. Sweeper indépendant pousse vers HTTP avec retry. Garantit at-least-once + cohérence DB ↔ delivery.

  4. Retry exponentiel borné — 8 tentatives max sur 24 h : 1s → 5s → 30s → 2min → 10min → 1h → 6h → 24h. Au-delà : marqué FAILED + alerte tenant DSI + Slack interne.

  5. Headers standardisésX-VitaKYC-Event-Id, X-VitaKYC-Event-Type, X-VitaKYC-Tenant-Id, X-VitaKYC-Delivery-Attempt, X-VitaKYC-Idempotency-Key (pour deduplication tenant-side).

  • HMAC-SHA256 plutôt que mTLS : plus simple à intégrer pour un tenant (pas besoin de gérer une CA, juste un secret rotable). mTLS reste optionnel pour tenants exigeants (config opt-in).
  • HMAC-SHA256 plutôt qu’Ed25519 : compatible avec tous les langages tenant (Node/Python/Java/PHP) sans dépendance crypto avancée. Signature algébrique non nécessaire ici (pas de propriétés zero-knowledge requises).
  • Format inspiré de Stripe : convention massivement déployée et bien documentée chez les développeurs back-end. Réduit la friction d’intégration.
  • Replay 5 min : assez large pour absorber l’horloge skew entre serveurs, assez court pour bloquer un attaquant qui ré-émet un payload capturé.
  • Outbox : pattern industrie pour exactly-once cross-system (Spring Modulith, Debezium). Garantit qu’un event en DB finira sur HTTP, et qu’un event sur HTTP est en DB.
  • Retry exponentiel 24 h : couvre les pannes prolongées tenant (4 h moyenne d’incident côté banque MENA observée). Au-delà, panne structurelle qui mérite intervention humaine.
  • Headers standardisés : permet aux tenants d’implémenter une queue commune réutilisable inter-events.

Positives

  • API tenant-side simple : un seul template de vérification HMAC à coder.
  • At-least-once avec idempotence côté tenant via X-VitaKYC-Idempotency-Key.
  • Dashboard livraison centralisé : tenant peut voir “j’ai reçu 1 247 events ce mois, 3 échecs sur 24h”.
  • Audit : chaque webhook tracé avec status (PENDING, DELIVERED, FAILED), retry attempts, response codes.
  • Rotation de secret : tenant peut faire un rotate qui maintient les deux secrets actifs pendant la fenêtre de transition (24 h).

Négatives

  • +1 service à exploiter (webhook-svc) — RAM modeste (~200 MB) mais point of failure transversal.
  • Latence : webhook émis 1-3 s après l’event Kafka (sweeper poll 1 s + HTTP). Acceptable pour notifications, pas pour temps-réel critique.
  • Stockage outbox : ~1 KB par event × 1 000 events/j × 30 j retention = 30 MB/tenant/mois. Trivial.
  • Conservation outbox : 30 jours après DELIVERED, 90 jours après FAILED final. Au-delà, archivage MinIO.
  • mTLS optionnel : un tenant peut activer mTLS (cert pinning) en plus du HMAC. Pour banques tier-1.
  • Webhook URL config : par tenant, par event type. Chaque event peut être routé vers une URL différente (ex bio.verdict vers /kyc-callback, aml.alert vers /compliance-callback). UI admin tenant pour gérer.
  • Rate limit côté tenant : si tenant retourne 429 (rate limit), on respecte Retry-After header. Backoff additionnel.
  • Body format : JSON canonique RFC 8785 (clés triées, encoding strict). Le hash HMAC est calculé sur le body canonical, donc déterministe.
  • Failure notification : 3 échecs consécutifs → email DSI tenant. 24 h en FAILED → email + ticket support automatique.
  • Test endpoint : POST /v1/webhooks/test permet au tenant d’envoyer un faux event vers son URL pour valider la conf.
  • Replay GUI : admin tenant peut re-déclencher manuellement un webhook depuis l’historique outbox.
OptionPourquoi écartée
mTLS obligatoireFriction d’intégration tenant (CA + cert lifecycle). Acceptable en option, pas en obligatoire.
JWT signé Ed25519 dans bodyStandard moins courant que HMAC en webhooks, plus complexe côté tenant (parsing JWT vs comparaison HMAC).
AWS SNS / Webhook Relay payantCoût récurrent + dépendance externe. Réinventer en interne plus économique à notre échelle.
Pas de retry, fire-and-forgetInacceptable pour banque (on perd des notifications de décision case = un client client en limbo).
WebSockets push persistantDemande tenant de maintenir une connexion ouverte 24/7. Irréaliste pour banques avec pare-feu.
gRPC streamingNon supporté par défaut dans les stacks back-end banque (Java EE 8, .NET, etc.).
MetricMVP cibleV2 cible
Latence p95 (event Kafka → webhook delivered)≤ 3 s≤ 1 s
Taux de delivery success first attempt≥ 95 %≥ 99 %
Taux de delivery success après retry≥ 99,5 %≥ 99,9 %
Taux de FAILED final (24 h écoulées)≤ 0,5 %≤ 0,1 %
Disponibilité webhook-svc99,5 %99,9 %
Throughput≥ 1 000 webhooks/min≥ 10 000
  • Démo S04 : webhook-svc opérationnel sur 3 event types (case.decided, bio.verdict, sanctions.screening.completed)
  • Pilote S08 : 1 tenant TN, 500 webhooks/jour, ≥ 99 % delivery rate
  • Revue M+6 : sondage satisfaction intégrateurs tenants sur l’ergonomie de la spec

ADR-033 — Auth/AuthZ : Keycloak realms hybrides + OIDC + RBAC/ABAC + MFA TOTP/WebAuthn + step-up + audit signé

Section intitulée « ADR-033 — Auth/AuthZ : Keycloak realms hybrides + OIDC + RBAC/ABAC + MFA TOTP/WebAuthn + step-up + audit signé »

Statut : Accepté Date : 2026-04-28

L’auth est la fondation de tout le reste — toute erreur ici coûte cher en banque MENA :

  • BCT Circulaire 2018-07 impose 4-eyes pour décisions sensibles
  • BCT Circulaire 2017-08 §III LCB-FT exige traçabilité agents
  • RGPD art. 32 sécurité du traitement
  • ANSI Tunisie référentiel sécurité
  • ISO 27001 contrôles A.9 access control
  • NIST SP 800-63B authenticator assurance levels

VitaKYC a deux populations distinctes :

  1. Identités plateforme — agents compliance L1/L2/MLRO, DSI tenant, auditeur, dev (humans utilisant VitaKYC)
  2. Identité client final — la personne qui s’onboarde (KYC), session courte, pas un user “permanent”

Pour les humans plateforme, on a besoin : login + MFA + RBAC + ABAC + step-up + SSO + audit signé. Pour les clients KYC, c’est juste un session token éphémère lié à un parcours, pas une identité auth.

Architecture Keycloak 25.x comme IdP central, realms hybrides (1 single realm SaaS + 1 par banque tier-1), OIDC Authorization Code Flow + PKCE, RBAC + ABAC côté code via shared auth-client-jvm, MFA TOTP obligatoire + WebAuthn optionnel (SMS interdit pour rôles ≥ L2), step-up MFA pour 7 actions sensibles, audit log signé Ed25519 append-only.

6 décisions structurantes :

  1. Keycloak 25.x plutôt qu’Auth0/Okta SaaS, Authentik, ou build interne

    • Apache 2 license — distribution on-prem sans coût licence
    • Air-gap natif (impératif BCT pour banques)
    • OIDC + SAML 2.0 + OAuth2 standards
    • Multi-tenant via realms natifs
    • MFA natif (TOTP, WebAuthn, push via FreeOTP+)
    • SCIM 2.0 provisionning (V1)
    • Themes brandables tenant
    • Maturité éprouvée (100+ banques EU, plusieurs gov en TN/MA)
  2. Realm strategy hybride — single vitakyc-saas pour fintechs/light + 1 realm dédié par banque tier-1 qui veut son IdP fédéré (Azure AD banque, AD on-prem). Pattern éprouvé par GitLab Enterprise + Atlassian Cloud.

  3. OIDC Authorization Code Flow + PKCE côté frontend back-office + SDK Web. OAuth 2.0 Client Credentials Flow entre services (mTLS). JWT signed RS256 (Keycloak default). JWKS endpoint cache 1h Caffeine côté services consumers.

  4. RBAC + ABAC en pure Kotlin (shared/auth-client-jvm) — types Role, Seniority, Permission, AssuranceLevel, principal VitaKycPrincipal. Pure functions Authz.hasPermission, requireSeniority, requireSameTenant, requireStepUp. Réutilisable backend (Ktor middleware) + tests unitaires.

  5. MFA tier matrix :

    • TOTP obligatoire tous rôles (Google Authenticator, Authy, FreeOTP+) — AAL2
    • WebAuthn / Passkeys recommandé DSI + MLRO (YubiKey, biométrie device) — AAL3
    • Push mobile via FreeOTP+ optionnel V1
    • SMS = NON (déprécié NIST SP 800-63B 2017, vulnerable SS7) ; fallback agent_l1 uniquement si pression contractuelle, jamais L2/MLRO
    • Email magic link seulement pour onboarding, jamais auth récurrente
  6. Step-up MFA (TTL 15 min) pour 7 actions sensibles :

    • policies.publish (Risk Matrix, Sanctions, AML rules, Form Designer)
    • cases.confirm (4-eyes seconde signature)
    • audit.export ou audit.replay
    • secrets.rotate (webhooks, API keys, tenant signing keys)
    • users.manage (création/suppression)
    • sanctions.lists.refresh force-refresh
    • forms.publish
  • Keycloak air-gap : tenant on-prem TN ne peut pas dépendre d’un SaaS externe. Auth0/Okta = no go pour BCT tier-1. Build interne = death sentence sécurité.
  • Realms hybrides : tenant fintech sans IdP partage vitakyc-saas. Banque tier-1 avec son AD = realm dédié + federation OIDC. Compromis ops vs flexibilité.
  • OIDC Authorization Code + PKCE : standard OWASP recommandé pour SPA. Refresh token rotation absorbe les exfiltration token (one-shot).
  • RBAC en Kotlin pur : déterministe, testable, auditable. Pas de framework lourd type Spring Security qui capture les décisions dans des annotations magiques difficiles à auditer.
  • MFA TOTP + WebAuthn pas SMS : NIST recommande, BCT moderne accepte. SS7 attaques sont documentées en TN (cas Banque B 2023 reported). Refus politique du SMS = qualité produit.
  • Step-up 7 actions : aligné sur BCT 2018-07 dual-control + RGPD art. 32. Évite qu’un session token volé permette d’exfiltrer 1 an d’audit.

Positives

  • Conformité BCT + ANSI + ISO 27001 démontrable.
  • 1 seul IdP à apprendre/exploiter pour les agents plateforme.
  • Federation tenant tier-1 native (banque utilise son AD existant).
  • Audit trail signé Ed25519 append-only des events auth.
  • Step-up granulaire évite les abus de session.
  • Token refresh rotation = exfiltration mitigée.

Négatives

  • Coût d’exploit Keycloak : ~1 instance HA + 1 Postgres dédié, ~4 GB RAM minimum. Profil ops “data engineer Keycloak” requis ou consultant externe ~30 K USD/an.
  • Keycloak upgrade : versions 24 → 25 → 26 ont eu breaking changes (auth flow lib v3, realm export format). Tests nécessaires à chaque upgrade.
  • Federation tier-1 : chaque banque a un quirk (Azure AD legacy, AD avec multi-domaines, certs CA privés). Setup atelier 1-2 jours par tenant tier-1.
  • WebAuthn demande HTTPS partout (pas un problème en prod, mais en dev local nécessite cert auto-signé ou mkcert).
  • Token TTL : access 15 min, refresh 8h rotation, SSO session 8h max + inactivity 30 min logout, step-up 15 min. Service-to-service mTLS cert 12 mois rotation.
  • JWT claims VitaKYC standardisés : sub, tenant_id, tenant_realm, roles[], permissions[], skills[], seniority, mfa_amr[], mfa_acr, step_up_until, session_id, iat, exp. Cf Principal.kt.
  • Permissions catalog : 16 permissions resource:action — cases:read|claim|decide|confirm|bulk, policies:read|edit|publish, forms:read|edit|publish, sanctions:screen|admin, audit:read|replay|export, users:read|manage, webhooks:read|manage, tenant:read|manage. Étendable par module.
  • Roles plateforme : 7 — agent_l1, agent_l2, mlro, dsi_admin, compliance_officer, auditor, developer.
  • Federation V0 : OIDC tenant + SAML 2.0 tenant. V1 : LDAP/AD direct sync, SCIM 2.0 provisioning auto.
  • Self-service : agents peuvent reset password + enroll/reset MFA TOTP via Keycloak account console (thème brandé tenant). DSI peut gérer ses agents via UI back-office VitaKYC.
  • Brute force protection : Keycloak Admin Brute Force Detection activé — 10 tentatives échouées en 5 min → lockout 15 min. Sliding window.
  • Privileged Identity : DSI admin + MLRO doivent avoir WebAuthn enrolled en plus de TOTP. Enforcé policy realm.
  • Audit sortants : tous events Keycloak (login, logout, MFA, role assigned/revoked, password change) → Kafka topic auth.audit.v1 consommé par audit-svc qui appose signature Ed25519 + chainage hash. Append-only Postgres auth_audit_event 10 ans WORM.
  • Anti-CSRF : tokens anti-CSRF côté UI back-office (ASP.NET-style ou OWASP CSRF guard).
  • Cookies : Secure, HttpOnly, SameSite=Strict partout. Pas de localStorage pour les tokens (XSS risk).
OptionPourquoi écartée
Auth0 / Okta SaaSDX excellent mais : pas air-gap (no go BCT), lock-in, pricing par MAU explose à scale (~3 USD/MAU × 50 K agents = 150 K/an).
AuthentikPlus moderne UI mais moins éprouvé en banque, communauté plus petite. Valeur ajoutée vs Keycloak limitée.
Build interne 100 %Death sentence sécurité — auth est le domaine où on ne réinvente pas.
Single realm “vitakyc-saas” pour tousEmpêche federation tier-1 (banque veut Azure AD), policy MFA spécifique tenant impossible.
1 realm strict par tenantMultiplication ops pour fintechs light qui n’ont pas d’IdP. Pattern hybride retenu.
OAuth Implicit FlowDéprécié OWASP 2019, exposait les tokens dans URL fragment. Authorization Code + PKCE seulement.
JWT signing HMAC (HS256)Symmetric — chaque service a la clé partagée → un service compromis exfiltre la signing key. RS256 asymmetric : Keycloak signe avec private key, services valident avec JWKS public.
Spring Security annotationsMagie compile-time ; difficile à auditer (qui a accès à quoi ?) ; force Spring Boot ; couplage fort. Préférons Authz pur Kotlin via shared lib.
SMS comme fallback rôle ≥ L2NIST SP 800-63B déprécié 2017, attaques SS7 documentées en TN. Refus politique.
Step-up MFA optionnel par tenantRisque qu’un tenant désactive et viole BCT 2018-07 sans le savoir. Enforcé serveur-side, jamais désactivable par tenant.
Magic link email pour login récurrentOK onboarding initial, jamais récurrent (boîte mail compromise = tous les comptes accessibles).
MetricMVP cibleV2 cible
Latence p95 login (incl MFA)≤ 3 s≤ 1.5 s
Latence p95 JWT validation (côté services)≤ 5 ms≤ 2 ms
MFA enrollment rate (rôles ≥ L1)100 %100 %
WebAuthn enrollment rate (DSI + MLRO)≥ 50 %100 % obligatoire
Auth chain integrity rate100 %100 %
Disponibilité Keycloak99,5 %99,9 %
Token refresh latency p95≤ 200 ms≤ 100 ms
Brute force lockout precision (false lockout rate)≤ 0,5 %≤ 0,1 %
  • Démo S04 : login OIDC + MFA TOTP + RBAC fonctionnel sur 1 tenant test, audit signé chained.
  • Pilote S08 : 1 banque TN avec 5 agents L1 + 2 agents L2 + 1 MLRO + 1 DSI, MFA enrolled tous, 0 incident sécurité.
  • Revue M+6 : indicateurs lockout precision, MFA bypass attempts, WebAuthn adoption tier-1.
  • Trigger nouvel ADR : si on intègre Smart Card / certificat clientside (ANSI Tunisie tier-1), revoir flow auth.

ADR-034 — TCR (Tax Compliance & Reporting) : pipeline orchestrée FATCA Chapter 4 + CRS OCDE — classifier interne + générateurs XML signés

Section intitulée « ADR-034 — TCR (Tax Compliance & Reporting) : pipeline orchestrée FATCA Chapter 4 + CRS OCDE — classifier interne + générateurs XML signés »

Statut : Accepté Date : 2026-04-29

VitaKYC se positionne sur le triptyque KYC + AML + TCR. Les briques KYC et AML sont couvertes (ADR-007 Form Designer, ADR-025 Risk Matrix, ADR-028 pipeline biométrique, ADR-029 case mgmt, ADR-030 sanctions, ADR-031 transaction monitoring). Reste à figer la TCRTax Compliance & Reporting — qui couvre 2 obligations :

  1. FATCA (US Foreign Account Tax Compliance Act, IRS Chapter 4, 2010+) : tout établissement financier hors US doit identifier les US persons parmi ses clients et déclarer annuellement leurs comptes à l’IRS, soit directement (modèle IGA 2), soit via l’autorité fiscale locale (modèle IGA 1, cas de la Tunisie depuis 2014).
  2. CRS (Common Reporting Standard, OCDE 2014+) : équivalent multilatéral. Les FFI déclarent à leur autorité fiscale locale les comptes des résidents fiscaux étrangers ; l’autorité échange ensuite avec ses homologues. La Tunisie a signé en 2019 et a démarré les premiers échanges en 2020.

Aujourd’hui un POC poc-fatca-generator existe pour produire le XML FATCA conforme au schéma DGI Tunisie / IRS Pub 5124 v2.0. Mais il n’y a aucune spec engineering globale qui décrit comment :

  • Capturer les indicia FATCA/CRS lors de l’onboarding (Form Designer ADR-007 a la palette d’indicia mais pas la sémantique de classification).
  • Classifier un client comme US person / non-US, passive NFE / active NFE / FI, et déterminer ses résidences fiscales CRS.
  • Identifier les controlling persons sur les entités passives.
  • Calculer les soldes en fin d’année et les paiements de l’exercice (intérêts, dividendes, brut versé, brut sortant).
  • Orchestrer la collecte annuelle de tous les comptes déclarables d’un FFI tenant.
  • Générer les XML FATCA + CRS, les signer, les soumettre à la DGI Tunisie / IRS IDES.
  • Gérer les corrections et annulations (FATCA MessageRefId, CRS DocTypeIndic OECD2/OECD3).
  • Conserver les preuves WORM 10 ans (cf BCT et obligations fiscales).

Architecture orchestrée par un service tcr-svc qui pilote 4 sous-modules. Stratégie 100 % build interne — schémas publics OCDE/IRS, pas de différenciateur commercial.

┌─────────────────────┐
│ tcr-svc │
│ (Temporal worker) │
└──────────┬──────────┘
┌───────────┬─────────────────┼──────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────────┐ ┌──────────┐ ┌──────────┐
│indicia │ │classifier│ │balance- │ │xml-gen │ │submit- │
│collector│ │ -engine │ │aggregator │ │FATCA/CRS │ │adapter │
└─────────┘ └─────────┘ └─────────────┘ └──────────┘ └──────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Form Designer CPS+rules core banking FATCA v2.0 DGI TN portal
(ADR-007/26) YAML (Temenos/T24, IRS Pub 5124 + IRS IDES SFTP
versioned Sopra,…) CRS v2.0 OCDE + signatures

4 sous-modules :

  1. indicia-collector — orchestration capture des indicia FATCA + CRS via Form Designer (ADR-007), enrichis par le CPS (ADR-026). Liste figée des indicia (US birth, US address, US phone, US POA, US standing instructions, US joint, lieu de naissance non-déclaratif, résidences fiscales multiples, etc.).
  2. classifier-engine — règles déterministes versionnées (DSL Kotlin partagé Risk Engine ADR-025) :
    • Classification entité : US Person / Specified US Person / Active NFE / Passive NFE / Reporting FI / Non-Reporting FI (FATCA + CRS).
    • Détermination résidences fiscales CRS : un client peut avoir 1 à N résidences ; règles par pays.
    • Identification controlling persons : pour entités passives, recherche UBO via RNE + déclaration client + seuil 25 %.
  3. balance-aggregator — collecte annuelle des soldes au 31/12 + paiements de l’exercice par type (interest, dividends, gross proceeds, other). Source : connecteurs core banking (Temenos T24 ADR existant, Sopra Banking, Oracle FCC, etc.).
  4. xml-generator + submit-adapter — génère les XML FATCA et CRS conformes schémas, signe XAdES (Tunisie), soumet via portail DGI ou IRS IDES SFTP, gère AR (Acknowledgement Receipt) et corrections.
  • Schémas publics et stables : FATCA XML v2.0 (IRS Pub 5124, schéma fixé 2017) et CRS XML v2.0 (OCDE, schéma fixé 2019). Aucun fournisseur n’apporte de différenciateur — c’est de la sérialisation déterministe d’un modèle de données. Build interne = ownership total et zéro coût marginal.
  • Réutilisation du DSL Risk Engine : la classification (Active NFE vs Passive NFE par exemple — calcul revenus passifs / revenus totaux > 50 % et actifs passifs / actifs totaux > 50 %) est exactement le type de règle que le DSL Kotlin du Risk Engine sait exprimer. Réutiliser évite un 2e moteur de règles à maintenir.
  • Orchestration Temporal : la collecte annuelle d’un FFI tenant peut traiter 100 K à 10 M de comptes. Workflow long avec checkpoints, retries, parallélisation par batch — Temporal est déjà la décision pour ce besoin (ADR-001).
  • Conformité par construction : chaque XML généré référence sa version de schéma, sa version de règles classifier, le hash du dataset source. Audit BCT + DGI peut reproduire à n’importe quel exercice ultérieur.
  • CRS multi-juridiction : la Tunisie a CRS, mais nos prospects bancaires français, marocains, libanais, ivoiriens sont sur d’autres juridictions de soumission. Le générateur CRS doit être paramétré par ReceivingCountry — pas dur, mais nécessaire au design dès le départ.

Positives

  • Promesse produit KYC + AML + TCR enfin tenue côté engineering.
  • Coût marginal ≈ 0 par déclaration (vs 0,30–0,80 USD/compte chez certains fournisseurs reporting).
  • Air-gap couvert (XML écrits localement, soumission par export fichier USB si nécessaire — la DGI TN accepte le dépôt manuel).
  • Audit trail reproductible 10 ans.

Négatives

  • Maintenir 2 schémas XML + suivre les évolutions (IRS publie des amendments à Pub 5124 ; OCDE mise à jour son schéma tous les 3-4 ans).
  • Connecteurs core banking à câbler par tenant pour récupérer les soldes et paiements (déjà partiellement couvert : Temenos T24 POC, mais Sopra/FCC/Oracle FlexCube à faire au cas par cas).
  • Période de pic mars-avril (clôture exercice + soumissions) — capacité doit absorber.
  • MessageRefId stratégie : format {TIN_filer}-{year}-{seq} unique et reproductible. Stocké dans table d’idempotence. Permet correction (OECD2) ou suppression (OECD3) sans collision.
  • DocRefId par AccountReport : {TIN_filer}-{year}-{accountId}-{seq}. Référencé dans les corrections.
  • Signature : XAdES-BES sur le fichier final (exigence DGI TN). Clé tenant via Vault + KMS (cohérent ADR-006 listes AML signées).
  • Encrypted submission : pour IRS IDES, certificate-based encryption obligatoire (M3M). Pour DGI TN, dépôt portail HTTPS authentifié + signature XAdES.
  • Nil reports : si aucun compte déclarable dans l’année, émettre un NilReport (FATCA MessageType=FATCA1 avec NoAccountToReport). Idem CRS.
  • Pré-validation : XSD validation + business rules (cohérence dates, totals balance ≥ 0 ou flag, présence Controlling Persons sur Passive NFE) avant signature et soumission.
  • Period model : par défaut calendar year (1er janvier — 31 décembre). Configurable par tenant pour FFI dont l’exercice fiscal diffère.
  • Reporting Period start/end : 2026-01-01 / 2026-12-31. Soumissions FATCA dues 30 septembre N+1 (DGI TN), CRS dues 31 mai N+1 (DGI TN). Calendrier intégré au scheduler.
  • Storage des XML générés : MinIO (cf ADR-005), retention 10 ans WORM, classification INTERNAL_FISCAL (chiffrement at-rest enveloppe).
  • Pas de diffusion auto : l’envoi à la DGI/IRS est gated par double signature humaine (compliance officer + DAF) — analogue dual-control Form Designer publish (ADR-027). Workflow d’approbation explicite.
  • Mode Sponsor : un sponsor enregistré peut déclarer pour le compte de plusieurs FFI sponsorées. Schémas le supportent ; à exposer côté UI tenant si demande commerciale.
OptionPourquoi écartée
Sous-traiter à Sovos / Trans-Tax / FATCAManagerCoût annuel 30 K–80 K USD par tenant. Tue notre promesse pricing. Et casse l’air-gap.
Réutiliser le moteur de règles risque comme classifier sans wrapperPossible techniquement, mais le vocabulaire est différent (US person, FFI types) et l’audit fiscal exige une versionning séparé des règles classifier vs règles risque. Wrapper léger préférable.
Générer JSON puis convertir XML via XSLTAjoute une étape, des bugs. XML généré directement avec un builder typé Kotlin = plus simple à auditer.
MessageRefId basé UUIDCasse la traçabilité humaine (auditeur fiscal lit TIN-2026-001, pas un UUID). On garde UUID en interne mais MessageRefId reste lisible.
Soumission auto sans dual-controlRisque de 6 chiffres : transmettre 100 000 comptes erronés à l’IRS = sanctions §6041 + §1471. Dual-control obligatoire.
Générateur CRS en PythonL’écosystème VitaKYC est Kotlin/JVM. Cohérence opération + DSL classifier partagé avec Risk Engine.
MetricMVPV2
Latence génération XML 10 K comptes≤ 30 s≤ 10 s
Latence soumission DGI TN (round-trip portal)≤ 60 s p95≤ 30 s
Validation XSD pass rate (avant soumission)100 %100 %
Taux de rejet AR (AcceptedWithErrors ou Rejected)≤ 5 %≤ 1 %
Couverture règles classifier vs IRS Pub 5124≥ 95 %100 %
Couverture règles classifier vs OECD CRS XML User Guide≥ 95 %100 %
  • Démo S06 : un compte fictif US classifié Specified US Person, XML FATCA généré, validé XSD, signé XAdES, soumis sur sandbox DGI.
  • Pilote S12 : 1 FFI pilote tunisien soumet sa déclaration FATCA réelle.
  • Revue M+12 : à la première campagne complète mars-avril 2027.
  • Trigger nouvel ADR : si OCDE publie CRS XML v3.0, ou si IRS publie Pub 5124 v3.0 avec breaking changes, ou si la Tunisie change de modèle IGA.

ADR-035 — Observability : OpenTelemetry traces + Prometheus metrics + Loki structured logs + Grafana, lib partagée observability-jvm

Section intitulée « ADR-035 — Observability : OpenTelemetry traces + Prometheus metrics + Loki structured logs + Grafana, lib partagée observability-jvm »

Statut : Accepté Date : 2026-04-26

Le monorepo vitakyc-platform est en Sprint 0 — auth-svc, tenant-svc, mrz-svc, form-engine-jvm, auth-client-jvm activés. Avant d’activer les modules suivants (bio-svc, tx-monitoring-svc, case-mgmt-svc, etc.), il faut figer la stratégie d’observabilité : sans elle, chaque service activé crée sa propre dette opérationnelle (logs ad-hoc, métriques absentes, pas de traces, debug en production impossible).

Forces en tension :

  1. Due-diligence DSI banques MENA + UE — c’est le 1er point qu’un CIO BIAT/UIB/Attijari/BNP challenge avant signature. Sans dashboards SLO et alerting clair, la conversation prod ne va pas plus loin.
  2. Hétérogénéité du parc de services — Ktor (auth, tenant, mrz, à venir form-designer, bio, sanctions, case-mgmt, tx-monitoring), Temporal workers, OPA pour règles. Tous doivent émettre traces, métriques, logs cohérents.
  3. On-prem + air-gap — la stack observability doit pouvoir tourner entièrement en local du tenant, sans appel SaaS externe (Datadog, New Relic, Grafana Cloud sont exclus en MVP banque tunisienne).
  4. Coût — la facture observability monte vite si chaque service inclut une stack différente. Une lib partagée + une stack docker-compose + Helm de référence évite la prolifération.

Stratégie unique alignée OpenTelemetry, à 4 piliers, avec une lib shared/observability-jvm que chaque service installe en une ligne :

// dans Application.kt de chaque service
fun Application.module() {
configureObservability(serviceName = "mrz-svc", serviceVersion = "0.1.0")
// ... autres plugins
}
PilierOutilFormatBackend
TracesOpenTelemetry SDK (Java) auto-instrument Ktor + manuel TemporalOTLP gRPCTempo
MetricsMicrometer + registry PrometheusOpenMetricsPrometheus scraping /metrics
LogsLogback JSON encoder + MDC enrichi traceId/spanId/tenantIdJSON lineLoki (Promtail / OTLP logs
UI / AlertingGrafana dashboards provisionnés + Alertmanagerdashboards JSON versionnés dans repo

Composants infra de référence (docker-compose dev + Helm prod) :

otel-collector ──┬── tempo (traces)
├── prometheus (metrics)
└── loki (logs)
grafana ←── alertmanager

L’OTel Collector est le point unique d’entrée — chaque service envoie en OTLP gRPC sur 4317. Il fan-out vers les 3 backends. Avantage : changer un backend (passer Tempo → Jaeger, Prometheus → Mimir) ne touche aucun service applicatif.

ConventionRègleExemple
Nom de service<bounded-context>-svc ou <bounded-context>-jvmmrz-svc, form-engine-jvm
Resource attributes (OTel)service.name, service.version, deployment.environment, service.namespace=vitakycobligatoires
Span name HTTP server<METHOD> <route>POST /v1/mrz/parse
Metric namesnake_case + unitévitakyc_http_server_requests_seconds, vitakyc_mrz_parse_total
Cardinalitytenant_id permis ; jamais d’identifiants utilisateurs ou de PIIOK : tenant_id="TN-BANQUEX" ; KO : client_id="..."
Sampling tracestail-based — 100 % des erreurs + 100 % des slow > p95 + 5 % du resteconfigurable Collector
Logstoujours JSON, MDC : traceId, spanId, tenantId, userId, requestIdformat Logback LogstashEncoder

Chaque service expose au minimum les 4 golden signals (Google SRE) en métriques Prometheus standardisées (cf page engineering §6) :

  • Latency : histogram p50/p95/p99 par route
  • Traffic : RPS par route + status
  • Errors : ratio 5xx + 4xx classés
  • Saturation : pool DB, queue Kafka lag, mémoire JVM

Et au moins 1 KPI métier (dépend du service) : mrz_parse_checksum_score_distribution, bio_verdict_outcomes_total, risk_evaluation_level_total, aml_alerts_emitted_total, etc.

  • OTel est le standard CNCF désormais incontournable. Pas besoin de pari technologique.
  • Lib partagée = un seul endroit pour modifier conventions, sampling, exporters → sécurité.
  • Self-hosted Grafana stack : 0 coût SaaS, fonctionne air-gap, données 100 % chez le tenant. Helm chart maintenu (grafana/loki-stack, grafana/tempo-distributed, prometheus-community/kube-prometheus-stack).
  • Cardinality discipline dès le départ : interdiction PII en label = pas de dette future à nettoyer (problème classique : 6 mois après tu as 5 M de séries Prometheus à cause d’un userId mis par erreur).

Positives

  • Toute activation de module suit le pattern : 1 ligne, 0 question de design observability.
  • Due-diligence banque facilitée : un dashboard Grafana de référence par service + un dashboard agrégé tenant.
  • Air-gap & SaaS supportés via la même config.
  • Performance overhead < 3 % CPU à 5 % sampling.

Négatives

  • Stack opérationnelle additionnelle à gérer en prod (Prometheus retention, Tempo storage, Loki ingester) → runbooks dédiés.
  • Coût stockage : ~50 GB/mois/cluster pour 10 services à 100 RPS médian (estimation Tempo + Loki + Prometheus avec retention 30j).
  • Grafana provisioning à versionner dans le repo (sinon dérive entre dev / staging / prod).
  • OTel Java agent : option auto-instrument complète possible mais on préfère SDK manuel pour garder le contrôle des spans nommés et des attributs (l’agent ajoute beaucoup d’attributs HTTP qui explosent la cardinality).
  • Exemplars Prometheus : activés (lien metric → trace cliquable depuis Grafana). Coût marginal négligeable.
  • Trace ID 128 bits W3C (par défaut OTel) — interop avec curl / clients tiers via header traceparent.
  • Privacy : pas de body HTTP capturé en trace ni en log par défaut. Activable au cas par cas sous flag avec rédaction PII.
  • Retention par défaut : Prometheus 30j local + 1 an downsampled (Mimir/Thanos en prod V2), Loki 30j chaud + 90j froid S3, Tempo 30j.
  • Dashboards versionnés : repo vitakyc-platform/infra/observability/grafana/dashboards/ exporté JSON, provisionné automatiquement au démarrage Grafana.
  • Alertmanager : routes par sévérité (page → on-call PagerDuty/SMS, ticket → Slack, info → email digest). Définition des règles dans infra/observability/prometheus-rules.yaml.
OptionPourquoi écartée
Datadog / New Relic SaaSCoût ($30–100 / host / mois × 10+ services), non air-gap, RGPD complexifié pour banques TN/FR.
ELK stack (Elastic + Kibana)Lourd à exploiter (cluster Elasticsearch dimensionné), pas de tracing first-class, licence Elastic depuis 2021 problématique.
Stack séparée par pilier (Jaeger + Prometheus + Fluentd)Triple effort à câbler. Grafana stack (Tempo/Prom/Loki/Grafana) homogène, même UI, opéré par les mêmes Helm charts.
OTel collector Sidecar (1/pod)Surcharge ressources sur clusters edge banque. Préfère 1 collector par namespace ou 1 DaemonSet par node.
Lib observability par service (pas partagée)Tue la cohérence. Conventions divergent, cardinality explose, dashboard generique impossible.
Prometheus push gatewayAnti-pattern Prometheus pour services long-lived. Conservé uniquement pour les jobs batch (TCR, scheduled workers).
  • Sprint 1 : mrz-svc instrumenté avec observability-jvm, dashboard Grafana visible, traces propagées de bout en bout sur 1 requête HTTP.
  • Sprint 3 : 5 services activés tous instrumentés ; alerting Slack + email opérationnel.
  • Pilote S10 : SLO 99,5 % mrz-svc mesuré sur dashboard ; 0 incident sans alerte préalable.
  • Revue M+6 : revue rétention + coût stockage ; décider évolution Mimir/Thanos pour Prometheus long-term storage.
  • Trigger nouvel ADR : si un fournisseur SaaS observability devient un must commercial, ou si on bascule vers eBPF (Cilium Hubble, Pixie).

ADR-036 — Audit log centralisé : append-only event sourcing, signature chaînée HMAC, retention 10 ans WORM, lib partagée audit-client-jvm

Section intitulée « ADR-036 — Audit log centralisé : append-only event sourcing, signature chaînée HMAC, retention 10 ans WORM, lib partagée audit-client-jvm »

Statut : Accepté Date : 2026-04-30

Sprint 1 du monorepo vitakyc-platform est avancé : auth-svc, tenant-svc, mrz-svc, form-engine-jvm, auth-client-jvm, observability-jvm activés. Avant d’activer les modules métier critiques (bio-svc, case-mgmt-svc, tx-monitoring-svc, risk-matrix-svc), il faut figer la stratégie d’audit log : sans elle, chaque service crée sa propre trace d’audit (table audit_log ad-hoc, format différent, sans signature, sans retention contrôlée), ce qui crée trois failles bloquantes :

  1. Conformité BCT — Circulaire 2017-08 BCT exige la conservation 10 ans des actions sur les dossiers KYC + une non-altérabilité prouvable des journaux. Sans signature chaînée, un admin avec accès DB peut retoucher l’historique sans laisser de trace détectable.
  2. RGPD art. 30 + art. 32 — registre des activités de traitement + sécurité des traitements : il faut pouvoir produire en heures un journal complet par sujet (tenantId + userId) pour une demande DSAR ou un audit CNDP/CNIL.
  3. Forensics incident — quand un incident survient (compromission de session, fraude interne), un audit log unifié signé est le seul artefact défendable en justice.

Forces en tension :

  • Performance — auth-svc émet ~50 events/s en pic (logins, MFA, step-up). Le service appelant ne peut pas attendre la confirmation d’écriture audit.
  • Durabilité — un crash de l’audit-svc ne doit jamais perdre un event critique (suppression policy, publication form, validation manuelle case).
  • Air-gap & SaaS — le système doit fonctionner sans dépendance externe (S3, Datadog Audit, Splunk Cloud).
  • DSAR — récupérer 10 ans d’events pour un userId/tenantId en < 1 h.

Architecture audit-svc centralisé + lib partagée audit-client-jvm que chaque service utilise en 1 ligne :

auditClient.log(
action = "FORM_PUBLISHED",
actor = call.principal.userId,
tenantId = call.principal.tenantId,
resource = "form:FORM_KYC_INDIVIDUAL@v2.7.0",
severity = AuditSeverity.NOTICE,
details = mapOf("hash" to formHash, "signatures" to listOf(...))
)

4 piliers :

PilierChoixJustification
TransportHTTP POST /v1/audit/events (Ktor) en MVP, outbox local + retry exponentiel côté clientSimple, pas de dépendance Kafka pour démarrer. La lib bufferise localement et retente — un crash audit-svc ≠ perte event. V2 bascule possible sur Kafka topic vitakyc.audit.v1.
PersistencePostgreSQL table append-only (RLS multi-tenant ADR-002) + index (tenant_id, ts) + GIN sur details JSONBSimple. Postgres tient 100M events/an confortablement. Append-only enforced par REVOKE UPDATE/DELETE.
IntégritéSignature chaînée HMAC-SHA256 : chainHash[n] = HMAC(secret, chainHash[n-1] ‖ event[n].canonicalJson). Secret rotaté annuellement via Vault.Détection de manipulation rétroactive. Si un admin DBA modifie/supprime une ligne, la chaîne casse au prochain audit. Vérification batch quotidien + alarme.
Retention10 ans dans Postgres tier chaud (3 mois) + S3 WORM Object Lock tier froid (10 ans - 3 mois)Tier-froid économique, immutabilité légale (Object Lock COMPLIANCE mode). Restitution DSAR < 1 h via Athena ou index secondaire chaud.

Schéma table append-only :

CREATE TABLE audit_event (
event_id UUID PRIMARY KEY,
ts TIMESTAMPTZ NOT NULL,
tenant_id VARCHAR(64), -- nullable pour événements platform
actor_user_id VARCHAR(128), -- userId Keycloak ou "system"
actor_kind VARCHAR(16) NOT NULL, -- 'human' | 'system' | 'service'
service VARCHAR(64) NOT NULL, -- ex 'auth-svc'
service_version VARCHAR(32) NOT NULL,
action VARCHAR(128) NOT NULL, -- ex 'FORM_PUBLISHED', 'LOGIN_OK'
resource VARCHAR(256), -- ex 'form:FORM_X@v2.7'
severity VARCHAR(16) NOT NULL, -- 'INFO'|'NOTICE'|'WARN'|'ALERT'
outcome VARCHAR(32) NOT NULL, -- 'success'|'failure'|'denied'
ip_address INET,
user_agent TEXT,
trace_id VARCHAR(32), -- corrélation OTel (cf ADR-035)
request_id VARCHAR(64),
details JSONB, -- payload structuré application-specific
prev_chain_hash CHAR(64) NOT NULL, -- hex SHA-256 du précédent event
chain_hash CHAR(64) NOT NULL, -- HMAC(secret, prev || canonical(this))
hmac_key_id VARCHAR(32) NOT NULL -- ex 'audit-key-2026'
);
CREATE INDEX idx_audit_tenant_ts ON audit_event (tenant_id, ts DESC);
CREATE INDEX idx_audit_actor_ts ON audit_event (actor_user_id, ts DESC);
CREATE INDEX idx_audit_action_ts ON audit_event (action, ts DESC);
CREATE INDEX idx_audit_details ON audit_event USING gin (details);
-- Append-only enforced
REVOKE UPDATE, DELETE ON audit_event FROM app_role;
ALTER TABLE audit_event ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON audit_event
USING (tenant_id IS NULL OR tenant_id = current_setting('app.tenant_id'));

Convention <DOMAIN>_<VERB> en SCREAMING_SNAKE :

DomaineEvents
AuthLOGIN_OK, LOGIN_FAIL, MFA_REQUESTED, MFA_OK, MFA_FAIL, STEP_UP_OK, LOGOUT, TOKEN_REFRESH, PASSWORD_RESET
TenantTENANT_CREATED, TENANT_SUSPENDED, TENANT_REACTIVATED, TENANT_DELETED
Form DesignerFORM_DRAFT_SAVED, FORM_PUBLISHED, FORM_ACTIVATED, FORM_VERSION_DELETED
Risk MatrixPOLICY_PUBLISHED, POLICY_ACTIVATED, POLICY_SHADOWED, POLICY_BACKTEST_RUN
BiometricKYC_VERIFICATION_STARTED, KYC_VERIFICATION_COMPLETED, KYC_VERIFICATION_FAILED, LIVENESS_FAIL, FACE_MATCH_FAIL
Case MgmtCASE_CREATED, CASE_ASSIGNED, CASE_DECISION_APPROVED, CASE_DECISION_REJECTED, CASE_ESCALATED
AML / SanctionsSCREENING_RUN, SCREENING_HIT_DETECTED, SCREENING_HIT_DISPOSED, LIST_UPDATED
WebhooksWEBHOOK_DELIVERED, WEBHOOK_FAILED_RETRYING, WEBHOOK_DEAD_LETTERED
AdminUSER_CREATED, USER_DEACTIVATED, ROLE_GRANTED, ROLE_REVOKED, CONFIG_CHANGED
DataDATA_EXPORTED, DSAR_REQUESTED, DSAR_FULFILLED, RETENTION_PURGED

Tout event critique (severity ∈ {WARN, ALERT}) est aussi propagé via webhook tenant (cf ADR-032) si configuré.

  • HTTP > Kafka en MVP — la stack dev a Kafka mais l’ergonomie HTTP est plus simple à câbler (pas de schema registry, pas de partition strategy à figer). Au-delà de 50 events/s par service, on bascule sur Kafka topic.
  • Outbox local côté client — la lib audit-client-jvm persiste les events dans une queue locale (mémoire bornée 10 K + spill disk H2/SQLite) et les flush async vers audit-svc. Garantit que un crash audit-svc ne perd jamais un event si le service appelant survit assez longtemps pour vider la queue.
  • Signature chaînée — pattern éprouvé (Bitcoin chain, Hyperledger, Notary). Rotation du secret HMAC tous les 12 mois par Vault (cf ADR-005), avec re-signing rolling sur les 12 derniers mois pour permettre rotation sans rupture.
  • Postgres + S3 WORM — pas de dépendance commerciale (Splunk, Datadog Audit), 100% air-gap compatible. S3 Object Lock COMPLIANCE mode est supporté par MinIO on-prem (cf ADR-005).
  • RLS multi-tenant — un tenant ne peut JAMAIS lire l’audit log d’un autre tenant via l’API standard (RLS bloque). Seul un endpoint admin super-tenant peut faire des queries cross-tenant (audité lui-même).

Positives

  • Toute action métier sensible est tracée avec un effort de 1 ligne par caller.
  • Conformité BCT 10 ans + RGPD art. 30 défendable.
  • DSAR fulfilment < 1 h grâce aux index et au tier chaud 3 mois.
  • Forensics : signature chaînée détecte toute manipulation rétroactive.
  • Pas de coût SaaS récurrent.

Négatives

  • Volume Postgres : ~5 GB/an pour 1 M events ; gérable mais à monitorer.
  • Maintenance des keys HMAC : Vault doit tourner ; sans Vault, fallback audit-key-default (warning visible).
  • Vérification chain quotidienne ajoute une charge CPU (~30 min batch sur 1 M events).
  • Outbox client mémoire : si le service crash avant flush, les events en mémoire sont perdus → mitigation par spill disk (H2 file).
  • Severity à 4 niveaux seulement (INFO, NOTICE, WARN, ALERT) — au-delà la cardinalité n’apporte rien pour l’audit.
  • Outcome obligatoire : success / failure / denied — sans cela les analyses fraud ratio sont impossibles.
  • PII rule : actor_user_id et ip_address sont des données personnelles → soumis aux retention policies + DSAR. details JSONB ne doit jamais contenir de mot de passe, token, CVV, photo, MRZ. Validation côté client (regex deny + size limit 16 KB).
  • Async fire-and-forget par défaut ; mode awaitConfirmation = true pour les events critiques (publication policy, suppression tenant).
  • Pas de modification des events : aucune API UPDATE/DELETE. Pour corriger un fait, on émet un nouvel event <ACTION>_CORRECTED qui référence l’event_id original.
  • Trace correlation : chaque event porte son trace_id et request_id (cf ADR-035) → cliquable depuis Grafana → Tempo.
  • Anti-PII guard côté lib : la lib refuse au build un appel qui contient des keys interdites (password, secret, token, cvv, …). Cf LabelGuard de observability-jvm — même approche.
  • Vault key rotation : audit-key-2026, audit-key-2027, etc. Tous les events portent leur hmac_key_id pour permettre la vérification même après rotation.
OptionPourquoi écartée
Splunk Audit / Datadog Audit Trails / AWS CloudTrailCoût récurrent ($30K+/an pour notre volume), pas air-gap, RGPD complexe pour banque TN/FR.
Audit log = Kafka topic + ksqlDBPlus complexe à opérer (Kafka cluster + Confluent Schema Registry + ksqlDB compute). MVP n’a pas le besoin. Migration possible V2.
Audit log = Loki structured logsLoki est un store de logs, pas un store transactionnel. Append-only ne suffit pas — on a besoin d’index par tenant/actor/action et de signature chaînée.
Postgres WAL streaming → S3 directlyPas de séparation logique audit vs autres tables. Restitution complexe.
Pas de signature chaînéeÉchec total pour défendre intégrité en justice. Coût d’ajout incrémental énorme rétroactivement (re-signer tout l’historique). Faut le faire dès J0.
Audit dans chaque service (table dédiée par service)Casse la requête transversale “qu’a fait userX dans les 24h”. Tue la conformité DSAR.
Solution maison custom merkle treeSur-ingénierie. HMAC chain suffit pour le besoin (linéaire, immutable, vérifiable). Merkle tree ajoute complexité sans bénéfice ici.
  • Sprint 2 : audit-svc activé, ingestion HTTP fonctionnelle, intégration depuis auth-svc (login, MFA, step-up).
  • Sprint 3 : intégrations tenant-svc, mrz-svc, batch verification chain quotidien.
  • Pilote S10 : 1 banque TN avec 100 K events/jour, query DSAR test < 30 min, vérification chain 0 alarme.
  • Revue M+6 : volumes mesurés ; décider migration Kafka si > 50 events/s/service.
  • Trigger nouvel ADR : si rachat / fusion exige interop avec un audit log tiers (Splunk, Cribl), ou si un régulateur impose une norme spécifique (ex. CSOC SOC 2 Type II).

ADR-037 — Convention de nommage JSON wire : snake_case partout

Section intitulée « ADR-037 — Convention de nommage JSON wire : snake_case partout »

Statut : Accepté Date : 2026-05-01

Au démarrage du monorepo vitakyc-platform, les exemples JSON publiés dans la doc et les DTOs Kotlin sérialisés ne suivaient pas une convention unique :

  • Certains documents (auth-system.md ligne 158, mobile-sdk-integration.md, observability.md) montrent des payloads en tenant_id, user_id, created_at.
  • D’autres (form-designer.md, risk-engine.md, client-profile-schema.md, biometric-pipeline.md) montrent les mêmes champs en tenantId, userId, createdAt.
  • Les DTOs Kotlin de form-engine-jvm et form-designer-svc annotent déjà @SerialName("snake_case") pour le wire (form_id, tenant_id, published_at, cps_path, internal_only, step_id, value_from, aria_label, tab_index, visible_if_rule, error_message, prefill_from).

Ce flou crée trois risques :

  1. Intégration tenant cassée — un partenaire qui copie un exemple "tenantId" de la doc obtient une 400 face à un service qui attend "tenant_id" (et inversement).
  2. Outils générés divergents — clients OpenAPI, schémas JSON Schema, exemples Postman génèrent des shapes incompatibles selon la source utilisée.
  3. Audit forensics ambigu — corréler un event audit_log.details.tenantId avec un payload entrant tenant_id impose un mapping mental à chaque debug.

snake_case est la convention unique pour tous les noms de propriétés JSON sur le wire : requêtes HTTP entrantes, réponses, payloads webhook, events Kafka, payloads d’audit, contenus de colonnes JSONB indexées (audit_event.details, kyc_business_unit.raw_payload, etc.).

Périmètre :

CoucheConventionMécanisme
Identifiants Kotlin / Java in-processcamelCaseidiomatique JVM, inchangé
Sérialisation JSON wire (REST, webhooks, events Kafka, JSONB)snake_case@SerialName("snake_case") sur chaque propriété concernée
Path params URLcamelCase toléré (/v1/forms/{tenantId}/{formId})héritage Ktor, reste lisible
Query paramssnake_casealigne avec body
Headers HTTP customKebab-CaseRFC 7230 standard
Noms de métriques OTelsnake_casedéjà imposé par ADR-035
Noms de colonnes SQLsnake_casedéjà standard PostgreSQL
Noms de topics Kafkadotted.snake_casedéjà standard, ex vitakyc.audit.v1

Champs canoniques (à ne jamais réécrire en camelCase nulle part) :

tenant_id, user_id, form_id, case_id, policy_id, version, published_at, published_by, created_at, updated_at, deleted_at, cps_path, internal_only, step_id, field_id, rule_id, value_from, prefill_from, aria_label, tab_index, visible_if_rule, error_message, correlation_id, request_id, trace_id, span_id, event_id, chain_hash, prev_chain_hash, actor_user_id, actor_kind, service_version, ip_address, user_agent.

  • Cohérence avec OAuth/OIDC, Stripe, AWS API, Slack API — qui sérialisent tous en snake_case. Un développeur intégrateur reconnaît immédiatement le style et débogue sans surprise.
  • Aligne wire ↔ SQL ↔ Kafka topic ↔ métriques — un tenant_id traverse toute la stack sans rename. Les requêtes JSONB (details->>'tenant_id') et les filtres Kafka utilisent le même nom.
  • Coût de migration faible — la majorité des DTOs sont déjà annotés @SerialName("snake_case"). Le reste du travail est une correction d’exemples documentaires.
  • Outils existants compatibleskotlinx.serialization supporte nativement @SerialName ; Jackson supporte PropertyNamingStrategies.SNAKE_CASE au niveau ObjectMapper si jamais on doit s’y replier.

Côté code (JVM)

  • Tout nouveau DTO @Serializable exposé sur le wire annote explicitement chaque propriété multi-mots avec @SerialName("snake_case").
  • Une règle Detekt / un test unitaire peut détecter les @Serializable sans @SerialName sur des propriétés camelCase (à ajouter en sprint 2).
  • Les DTOs internes non sérialisés restent en camelCase natif Kotlin.

Côté documentation

  • Tous les exemples JSON (*.md engineering/, compliance/, architecture/) sont normalisés snake_case.
  • Les schémas OpenAPI générés à partir des DTOs Kotlin reflètent automatiquement le wire format via @SerialName.

Côté frontend (back-office/)

  • Les types TypeScript des DTOs reçus utilisent les clés snake_case telles quelles (response.tenant_id). Pas de rewrap automatique en camelCase — on garde la cohérence wire ↔ code TS.

Path params

  • L’exception camelCase autorisée pour les path params est volontairement étroite : Ktor génère naturellement les routes avec les noms de paramètres tels que déclarés dans la signature Kotlin ({tenantId}). Les rewriter en snake_case casserait le contrat pour zéro bénéfice.
OptionPourquoi écartée
camelCase partoutCasse l’alignement avec les colonnes SQL et les noms de métriques snake_case. Crée un mapping permanent dans tous les services pour persistance.
camelCase body + snake_case path/queryIncohérence visible à chaque endpoint. Pollue le code de mappers.
Pas de convention écrite (laisser au goût de chaque service)C’est l’état actuel. Crée les 3 risques décrits dans le contexte.
Adopter kebab-case JSON (RFC convention pour headers)Non standard pour body JSON. Casse JS/TS (obj.tenant-id interdit).
@JsonNaming(SnakeCaseStrategy) au lieu de @SerialName annotation par annotationkotlinx.serialization n’a pas l’équivalent de Jackson PropertyNamingStrategies au niveau global. L’annotation explicite par champ est plus robuste : grep-able, pas de magie globale, override local possible (ex aria-label qui doit rester avec tiret).
  • Sprint 1 : ADR adopté ; exemples JSON de la doc normalisés ; règle Detekt / test à ajouter.
  • Pilote S10 : un client tenant intègre via SDK et webhooks ; vérifier qu’aucun rapport d’incompatibilité de nommage ne remonte.
  • Trigger nouvel ADR : si un partenaire majeur (banque tier-1, régulateur DGI/BCT) impose un autre format pour un endpoint spécifique, on documente l’exception sans réviser la règle générale.

  • ADR-013 — Stratégie de recherche full-text multilingue (arabe, français) — OpenSearch analyzers + plugin ICU.
  • ADR-014 — Real-time transaction streaming : Kafka Streams vs Flink pour transaction monitoring (cf. AML TxMon).
  • ADR-015 — Certificats mTLS gestion : Cert-Manager + CA privée interne.
  • ADR-016 — Mobile SDK size budget + lazy loading modèles ONNX (détaillé par device class).

Fin du document ADRs.