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é.
Sommaire
Section intitulée « Sommaire »- ADR-001 — Moteur de workflow : Temporal.io
- ADR-002 — Multi-tenant : shared schema + Row Level Security PostgreSQL
- ADR-003 — OCR : build hybride (moteur interne + fallback commercial)
- ADR-004 — Moteur de règles métier : DSL Kotlin + OPA pour politiques
- ADR-005 — Stockage des documents : MinIO / S3 compatible + couche d’abstraction
- ADR-006 — Gestion des listes AML : pipeline incrémental avec support air-gap
- ADR-007 — Form Designer : scope MVP (no-code, configurable par tenant)
- ADR-008 — Génération PDF : Apache PDFBox + Gotenberg
- ADR-009 — i18n : ICU MessageFormat + CSS logique + RTL arabe
- ADR-010 — Distribution SDKs : NPM + Maven Central + Swift PM
- ADR-011 — E-signature : DocuSign + Yousign FR + TunTrust/ANCE (Tunisie) + module natif SES + option QES
- ADR-012 — Zero-downtime upgrades on-prem + Oracle + air-gap
- ADR-024 — Stratégie mobile : SDK-first + tenant resolution SaaS / on-prem / hybride
- ADR-025 — Modèle de risque client : matrice multi-dimensionnelle + RBA paramétrable
- ADR-026 — Intégration Form Designer ↔ Risk Matrix via Client Profile Schema partagé
- ADR-027 — Form Designer : moteur d’exécution déclaratif (JSON canonique + DSL Kotlin + runtime isomorphe)
- ADR-028 — Pipeline biométrique : orchestration capture + MRZ + liveness + face match (build interne + fallback commercial)
- ADR-029 — Case Management : workflow agent compliance, file d’attente skill-based, SLA escaladable, audit trail append-only
- ADR-030 — Sanctions screening : OpenSearch unique packagé + re-ranker Kotlin + RCA dénormalisé ≤ 2 sauts + audit log signé
- ADR-031 — Transaction Monitoring streaming : Kafka Streams + rules DSL Kotlin + sliding windows + alert dedup + outbox
- ADR-032 — Webhooks signés : HMAC-SHA256 + replay protection + outbox + retry exponentiel
- ADR-033 — Auth/AuthZ : Keycloak realms hybrides + OIDC + RBAC/ABAC + MFA TOTP/WebAuthn + step-up + audit signé
- ADR-034 — TCR (Tax Compliance & Reporting) : pipeline orchestrée FATCA Chapter 4 + CRS OCDE — classifier interne + générateurs XML signés
- ADR-035 — Observability : OpenTelemetry traces + Prometheus metrics + Loki structured logs + Grafana, lib partagée
observability-jvm - ADR-036 — Audit log centralisé : append-only event sourcing, signature chaînée HMAC, retention 10 ans WORM, lib partagée
audit-client-jvm - ADR-037 — Convention de nommage JSON wire :
snake_casepartout
ADR-001 — Moteur de workflow : Temporal.io
Section intitulée « ADR-001 — Moteur de workflow : Temporal.io »Statut : Proposé — à valider par l’équipe technique au kickoff Date : 2026-04-22 Décideurs : Tech Lead, Architecte
Contexte
Section intitulée « Contexte »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.
Décision
Section intitulée « Décision »Adopter Temporal.io (self-hosted) comme moteur de workflow pour tous les traitements long-running.
Justification
Section intitulée « Justification »- 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.
Conséquences
Section intitulée « Conséquences »Positives
- Réduction du code de plomberie (retry, état, persistance) de ~30-40 %.
- Chaînage FATCA 1/3 facile à implémenter comme un workflow :
fatca3puisworkflow.sleep(untilDgiIrsWindowClosed)puisfatca1. - 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.
Alternatives considérées
Section intitulée « Alternatives considérées »| Option | Pourquoi 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 Functions | Cloud-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 / Dagster | Orientés data pipelines, pas orchestration métier long-running avec signals. |
Références
Section intitulée « Références »- 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
Contexte
Section intitulée « Contexte »VitaKYC doit supporter trois modes de déploiement :
- SaaS multi-tenant partagé — plusieurs clients sur la même infrastructure.
- SaaS dédié — infra dédiée par client (enterprise).
- 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.
Décision
Section intitulée « Décision »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).
Justification
Section intitulée « Justification »- 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.
Implémentation
Section intitulée « Implémentation »-- Chaque table métier porte tenant_idCREATE TABLE kyc_case ( case_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, -- ...autres colonnes);
-- RLS activéeALTER 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 connexionCREATE POLICY tenant_isolation ON kyc_case USING (tenant_id = current_setting('app.current_tenant')::uuid);
-- L'app fait :-- SET app.current_tenant = '<uuid>';Conséquences
Section intitulée « Conséquences »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_tenantavant 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.
Alternatives considérées
Section intitulée « Alternatives considérées »| Option | Pourquoi non retenue |
|---|---|
| Database per tenant | Coût opérationnel trop élevé au MVP ; retardera le time-to-market. |
| Schema per tenant | Migrations 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. |
Exception prévue
Section intitulée « Exception prévue »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
Contexte
Section intitulée « Contexte »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.
Décision
Section intitulée « Décision »Stratégie hybride avec routage :
- 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. - 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).
- 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).
Justification
Section intitulée « Justification »- 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.
Architecture
Section intitulée « Architecture » ┌────────────┐doc ────▶│ ocr-svc │ │ │ │ 1. route │ └─────┬──────┘ │ ┌───────┴────────┐ ▼ ▼ ┌───────────┐ ┌──────────────┐ │ moteur │ │ adapter │ │ interne │ │ commercial │ │ (ONNX) │ │ (Google, │ │ │ │ Regula, …) │ └────┬──────┘ └──────┬───────┘ │ score < seuil │ └────────┬─────────┘ ▼ ┌────────────┐ │ résultat │ │ consolidé │ └────────────┘Conséquences
Section intitulée « Conséquences »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).
Alternatives considérées
Section intitulée « Alternatives considérées »- 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.
Métriques cibles
Section intitulée « Métriques cibles »- 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
Contexte
Section intitulée « Contexte »VitaKYC a deux natures de règles à gérer :
- 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.
- 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.
Décision
Section intitulée « Décision »Approche double :
- Règles métier → DSL 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 / compliance → Open Policy Agent (OPA) avec policies Rego, déployé en sidecar
opasur les services.
Justification
Section intitulée « Justification »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).
Exemple — règle métier KYC (Kotlin)
Section intitulée « Exemple — règle métier KYC (Kotlin) »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")) } }}Exemple — policy OPA (Rego)
Section intitulée « Exemple — policy OPA (Rego) »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 cibleallow { 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}Conséquences
Section intitulée « Conséquences »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.
Alternatives considérées
Section intitulée « Alternatives considérées »- 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
Contexte
Section intitulée « Contexte »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).
Décision
Section intitulée « Décision »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-storeencapsule l’accès : le code métier n’appelle jamais directement l’API S3.
Justification
Section intitulée « Justification »- 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.
Organisation des buckets
Section intitulée « Organisation des buckets »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.encConsequences
Section intitulée « Consequences »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é.
Alternatives considérées
Section intitulée « Alternatives considérées »- 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
Contexte
Section intitulée « Contexte »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.
Décision
Section intitulée « Décision »Pipeline en quatre étapes centralisé côté VitaKYC, puis distribution :
- Ingestion : poller les sources officielles (OFAC, UN, EU…) et les APIs des providers ; normaliser vers un format commun
VKL v1(VitaKYC Lists). - Normalisation : canonicalisation des noms (arabe → latin + unicode normalization), extraction des identifiants (dates de naissance, nationalité, AKA), scoring de confiance.
- 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.
- Consommation :
aml-svcinterroge OpenSearch ; le pipeline de matching est identique quels que soient le mode de distribution.
Format VKL v1 (extrait)
Section intitulée « Format VKL v1 (extrait) »{ "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" } ]}Consequences
Section intitulée « Consequences »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.
Questions ouvertes
Section intitulée « Questions ouvertes »- 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).
Processus de révision
Section intitulée « Processus de révision »- 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
Contexte
Section intitulée « Contexte »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.
Décision
Section intitulée « Décision »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.
Justification
Section intitulée « Justification »- 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.
Conséquences
Section intitulée « Conséquences »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.
Décisions secondaires
Section intitulée « Décisions secondaires »- 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
ACTIVEouSHADOWne peut pas être supprimé. La suppression est proposée aprèsdeprecated=truependant 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é parprofile-schema-svcpour tenir à jour le CPS. Payload :{tenantId, formId, version, addedFields[], removedFields[], renamedFields[]}. Compliance est notifié des changements structurants.
Revue de cette décision
Section intitulée « Revue de cette décision »- À 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.
Alternatives considérées et écartées
Section intitulée « Alternatives considérées et écartées »| Option | Pourquoi écartée |
|---|---|
| YAML templates + code-only customization | Rejeté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é. |
Références
Section intitulée « Références »VitaKYC_Cahier_des_Charges.md§4.1.0VitaKYC_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
Contexte
Section intitulée « Contexte »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).
Décision
Section intitulée « Décision »- 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.
Justification
Section intitulée « Justification »- 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.
Alternatives écartées
Section intitulée « Alternatives écartées »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
Contexte
Section intitulée « Contexte »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).
Décision
Section intitulée « Décision »- 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,englishpour recherche multilingue.
Standards
Section intitulée « Standards »- BCP 47 (
ar-TN,fr-TN,ar-SA,en-US). - ISO 4217 monnaies.
- ISO 8601 dates/heures en API.
Conséquences
Section intitulée « Conséquences »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
Décision
Section intitulée « Décision »| SDK | Canal public | Canal enterprise |
|---|---|---|
| Web TypeScript | @vitakyc/sdk-web sur npmjs.com | GitHub Packages privé |
| iOS Swift | Swift Package Manager depuis repo public | CocoaPods privé (spec repo) |
| Android Kotlin | Maven Central io.vitakyc:sdk-android | JFrog 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.
Deprecation policy
Section intitulée « Deprecation policy »6 mois minimum de support après release d’une MAJOR. Headers Deprecation + doc changelog publique.
Alternatives écartées
Section intitulée « Alternatives écartées »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)
Contexte
Section intitulée « Contexte »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
Décision
Section intitulée « Décision »Approche multi-provider configurable par tenant avec 5 options :
- DocuSign — provider par défaut Growth / Enterprise, couverture mondiale, API mature.
- Yousign / Universign (France) — eIDAS AES / QES, moins cher que DocuSign sur la zone UE.
- 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.
- 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.
- Option QES on-prem — Cryptolog Evidency, Signaturit ou Universign QES pour clients exigeant PKI dédiée.
Pourquoi TunTrust / ANCE
Section intitulée « Pourquoi TunTrust / ANCE »- 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).
Architecture d’intégration TunTrust / ANCE
Section intitulée « Architecture d’intégration TunTrust / ANCE »- 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é.
Critères par cas d’usage (mis à jour)
Section intitulée « Critères par cas d’usage (mis à jour) »| Cas | Niveau exigé | Provider recommandé |
|---|---|---|
| Consentement GDPR au KYC | SES | Module natif VitaKYC |
| W-8BEN individu | SES + timestamp | Module natif VitaKYC |
| W-8BEN-E entité — tenant tunisien | AES/QES locale | TunTrust / ANCE |
| W-8BEN-E entité — tenant UE | AES | DocuSign / Yousign |
| Mandat SEPA — tenant FR | AES | Yousign |
| Contrat compte bancaire TN | QES tunisienne | TunTrust / ANCE |
| Contrat compte bancaire FR | QES eIDAS | Yousign QES / Universign |
| Dépôt FATCA à IDES (cachet électronique IF) | QES tunisienne | TunTrust / ANCE (certificat obligatoire IDES) |
| Signature de rapport BCT généré | QES tunisienne | TunTrust / ANCE |
Conséquences
Section intitulée « Conséquences »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.
Hors périmètre VitaKYC (à noter)
Section intitulée « Hors périmètre VitaKYC (à noter) »- 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
Contexte
Section intitulée « Contexte »Clients banque on-prem exigent PostgreSQL ou Oracle. SLA 99,9 % requis. Certains environnements sont air-gap (pas de phone-home possible).
Décision
Section intitulée « Décision »Stratégie blue-green au niveau pod + migrations DB backward-compatibles :
- Rolling update Kubernetes (maxUnavailable=0, maxSurge=1) par défaut.
- Flyway multi-SGBD (PostgreSQL + Oracle + SQL Server) avec règle absolue : toute release N+1 fonctionne avec le schéma N (pattern expand/contract).
- Tests CI obligatoires : image N+1 bootée sur schéma N avant approbation release.
- Runbook trimestriel on-prem livré avec chaque release, incluant pré-requis Oracle (privilèges, tablespaces, backups).
- Rollback documenté step-by-step, testé sur env client de test avant upgrade prod.
- Bundle air-gap signé cosign : images OCI + Helm chart + migrations SQL + release notes, transféré sur support amovible chiffré si requis.
Alternatives écartées
Section intitulée « Alternatives écartées »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
Contexte
Section intitulée « Contexte »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 :
- 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).
- 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.
- 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 :
| Éditeur | App standalone ? | Pattern retenu |
|---|---|---|
| Onfido | Non (client) | SDK embarqué dans app banque |
| Jumio | Non (client) | SDK + Web SDK PWA |
| Sumsub | Non (client) | SDK + WebSDK + NFC module |
| Veriff | Non (client) | SDK + In-house flow web |
| IDnow | Non (client) | SDK + VideoIdent service web |
Tous ont une app standalone réservée aux agents (back-office mobile), pas au client final.
Décision
Section intitulée « Décision »Trois surfaces mobiles VitaKYC, aucune app “VitaKYC” standalone grand public.
| # | Surface | Publisher store | Utilisateur | Vecteur de distribution |
|---|---|---|---|---|
| 1 | SDK natif iOS/Android + React Native + Flutter | La banque (dans son app) | Client final KYC | Package registry (Maven Central, Swift PM, CocoaPods, NPM) |
| 2 | Web SDK / PWA branded | — (URL) | Client final KYC sans app banque (ou iOS sans NFC) | Lien SMS / QR / magic-link |
| 3 | VitaKYC Agent Mobile (standalone) | VitaKYC | Agents 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.
Patterns de tenant resolution — SaaS partagé
Section intitulée « Patterns de tenant resolution — SaaS partagé »// Intégration dans l'app banque — BuildConfig.ktVitaKYC.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é. sessionProviderretourne 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).
Patterns — SaaS dédié (single-tenant SaaS)
Section intitulée « Patterns — SaaS dédié (single-tenant SaaS) »Identique au SaaS partagé, mais apiBase pointe vers l’instance dédiée : https://<banquex>.kyc.vitakyc.com.
Patterns — On-premise
Section intitulée « Patterns — On-premise »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-Idvalidé 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 dynamiquesLa 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/sessionsVitaKYC → { session_token (JWT 30min), url: "https://kyc.banquex.tn/s/ABC123" } ↓ SMS au clientClient → Safari/Chrome → kyc.banquex.tn (CNAME → vitakyc-cdn.com) ↓ reverse lookup Host header OU claim JWTTenant résolu → PWA branded charge → flow KYC identique SDK natifLimite iOS : pas de NFC eMRTD en web (Web NFC = Android Chrome only). Si NFC obligatoire → SDK natif seul chemin.
Résolution côté API — 3 couches
Section intitulée « Résolution côté API — 3 couches »| Couche | Mécanisme | Rôle |
|---|---|---|
| L7 ingress (Traefik / nginx) | Host header + path prefix | Route vers namespace K8s tenant-TN-BANQUEX (SaaS shared) |
| API gateway (Kong / Envoy) | Claim tenant_id du JWT | Enforce RBAC + rate limit par tenant |
| Service applicatif | tenant_id context propagé via Kotlin MDC + WebFilter | Chaque 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.
Conséquences
Section intitulée « Conséquences »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/runtimepour 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.
Décisions secondaires
Section intitulée « Décisions secondaires »- 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_highpour 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).
Edge cases spécifiés
Section intitulée « Edge cases spécifiés »| Cas | Comportement |
|---|---|
| Bascule de tenant à chaud | Interdite — réinstallation de l’app banque |
| Session expirée pendant l’onboarding | Refresh 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 courant | Intent handler vérifie sessionToken.sub == currentBankUser, sinon logout forcé |
| Downgrade HTTP | HSTS PWA + NSAppTransportSecurity strict iOS + cleartextTrafficPermitted="false" Android |
| Device rooted/jailbroken | Flag device_risk=high, décision policy tenant (bloquant ou review manuelle) |
| App en arrière-plan pendant liveness | Reprise caméra impossible sur iOS → user recommence cette étape uniquement (state saved) |
Alternatives considérées et écartées
Section intitulée « Alternatives considérées et écartées »| Option | Pourquoi écartée |
|---|---|
| App VitaKYC standalone grand public + saisie code tenant | Cassure 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 natif | Pas 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 login | Inutilisable : 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 de cette décision
Section intitulée « Revue de cette décision »- 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.
Références
Section intitulée « Références »- 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
Contexte
Section intitulée « Contexte »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 :
- 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.
- 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.
- 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.
Décision
Section intitulée « Décision »VitaKYC livre un risk engine paramétrable par tenant basé sur :
- 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.
- 4 niveaux de risque — LOW [0-25] · STANDARD [26-50] · HIGH [51-80] · PROHIBITED [81-100]
- Override rules —
mustProhibit,mustHigh,mustLowqui outrepassent le score calculé (ex : match OFAC = PROHIBITED quel que soit le reste) - Explainability obligatoire — chaque décision émet un JSON structuré : contribution de chaque dimension, raison, règles déclenchées, mitigations requises
- 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)
- 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)
DSL — exemple de signature
Section intitulée « DSL — exemple de signature »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.
Conséquences
Section intitulée « Conséquences »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
Décisions secondaires
Section intitulée « Décisions secondaires »- 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 Kafkarisk.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.
Alternatives considérées et écartées
Section intitulée « Alternatives considérées et écartées »| Option | Pourquoi écartée |
|---|---|
| ML end-to-end pour scoring | Non 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 dur | Impossible de vendre (chaque banque a sa politique). Releases mensuelles au rythme des banques = impossible. |
| Drools / moteur commercial | Licences 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. |
Revue de cette décision
Section intitulée « Revue de cette décision »- 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.
Références
Section intitulée « Références »- 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
Contexte
Section intitulée « Contexte »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 :
| Option | Description | Verdict |
|---|---|---|
| A. Fusion complète | Le Risk Matrix est un onglet du Form Designer (même UI, même data model, même publication) | Écarté |
| B. Isolation totale | Form 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 commun | Retenu |
Pourquoi pas la fusion (A)
Section intitulée « Pourquoi pas la fusion (A) »La fusion casse cinq invariants critiques :
- 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.
- 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.
- 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.
- 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.
- 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.
Pourquoi pas l’isolation totale (B)
Section intitulée « Pourquoi pas l’isolation totale (B) »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 > 0sur 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.
Décision
Section intitulée « Décision »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│ └──────────────────┘ └──────────────────┘Concrètement
Section intitulée « Concrètement »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.publishedconsommé 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.professionvient deform: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
Contrat CPS (format)
Section intitulée « Contrat CPS (format) »{ "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" } ] } ]}Conséquences
Section intitulée « Conséquences »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
Décisions secondaires
Section intitulée « Décisions secondaires »- 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.
Alternatives considérées et écartées
Section intitulée « Alternatives considérées et écartées »| Option | Pourquoi é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 schema | Overkill pour la taille du contrat (~KB) et la simplicité des consommateurs. Ajoute du tooling (gateway, federation spec) sans bénéfice ici. |
| JSONSchema standard strict | Considéré — utilisé en interne pour valider le contrat. Mais pas suffisant : il faut en plus la notion de source, usedBy, deprecated, sensitivity → extension custom. |
Revue de cette décision
Section intitulée « Revue de cette décision »- 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.
Références
Section intitulée « Références »- 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
Contexte
Section intitulée « Contexte »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 :
- 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).
- 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.
- 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).
Décision
Section intitulée « Décision »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-FT2. 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écapform-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.
Justification
Section intitulée « Justification »- 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èsfield("..."), mêmes catalogues. Un développeur qui maîtrise l’un maîtrise l’autre. - Mapping CPS enforced à la compilation : le DSL exige
cpsPathpour tout champ non-internal_only. Un formulaire qui omet cette mention casse à la compilation Kotlin, pas en runtime. Cohérent avec ADR-026.
Conséquences
Section intitulée « Conséquences »Positives
- Audit BCT facilité : une
FormDefinitionpubliée est un artefact JSON signé reproductible. - Backtest possible : on peut rejouer une
Submissionhistorique avec n’importe quelleFormDefinitiondu 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).
Décisions secondaires
Section intitulée « Décisions secondaires »- Versioning SemVer —
MAJOR.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.MINORcô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=truepeut être basculée mais jamais éditée. (Cf ADR-002, retention WORM.) - Préchauffage — au publish, le serveur compile le
FormDefinitionJSON 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
formVersionIdest figé sur laSubmissionpour 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.
Alternatives considérées et écartées
Section intitulée « Alternatives considérées et écartées »| Option | Pourquoi écartée |
|---|---|
| YAML au lieu de JSON canonique | Multi-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 / CMMN | Sur-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. |
Revue de cette décision
Section intitulée « Revue de cette décision »- 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.
Références
Section intitulée « Références »- ADR-007 — scope MVP Form Designer
- ADR-026 — contrat externe via CPS
- ADR-004 — DSL Kotlin moteur de règles
- ADR-009 — i18n FR/AR/EN
- Page engineering : Form Designer — moteur d’exécution
- POC : poc-form-designer
- Standards : JSON Canonicalization Scheme (RFC 8785), JOSE/COSE pour signature
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
Contexte
Section intitulée « Contexte »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 :
- Capture qualifiée d’un document d’identité (recto + verso si CNI, RFID si biométrique)
- OCR des champs visibles (déjà couvert par ADR-003)
- MRZ — extraction et vérification de la zone de lecture mécanique (ICAO 9303) avec ses 5 chiffres de contrôle
- Authenticité du document — détection de fraude (templates, patterns, hologrammes, MRZ vs OCR)
- Capture selfie + liveness detection (anti-spoof : photo, vidéo replay, masque 2D/3D, deepfake)
- 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.
Décision
Section intitulée « Décision »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 / JumioChoix de fond par sous-service :
| Service | Build interne MVP | Fallback commercial | Routage |
|---|---|---|---|
ocr-svc | Tesseract 5 + ONNX fine-tuné MENA | Google Vision / AWS Textract / Mistral OCR | seuil de confiance (cf ADR-003) |
mrz-svc | parseur ICAO 9303 (TD1, TD2, TD3) en Kotlin pur | aucun (interne suffit) | 100 % interne |
doc-auth-svc | template matching + heuristiques (V1) | Regula Document Reader SDK / Onfido document check | feature flag tenant — par défaut commercial au MVP |
liveness-svc | passive analysis (qualité, motion, blink) en MVP | Onfido Motion / Sumsub Liveness / Veriff (iBeta L2) | feature flag tenant — commercial recommandé MVP |
face-match-svc | InsightFace ONNX (ArcFace) — score cosine | Onfido / Sumsub / Regula Face SDK | seuil score interne ; fallback si score ambigu |
Justification
Section intitulée « Justification »- 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, etocr-svcinterne fonctionnent sans Internet.doc-auth-svcRegula a un mode SDK on-prem (binaire embarqué).liveness-svcinterne 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.
Conséquences
Section intitulée « Conséquences »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-parserlivré).
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).
Décisions secondaires
Section intitulée « Décisions secondaires »- 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_requiredet l’agent compliance tranche. - Score face match consolidé :
score_final = w_intern * score_arcface + w_commercial * score_commercialavec 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 enmanual_review_requiredplutô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.
Alternatives considérées et écartées
Section intitulée « Alternatives considérées et écartées »| Option | Pourquoi é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 MVP | iBeta 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 MVP | Passive 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 monolithique | Tue 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é. |
Métriques cibles
Section intitulée « Métriques cibles »| Metric | MVP cible | V2 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 liveness | certifié via fournisseur | interne en cours |
Disponibilité bio-svc | 99,5 % | 99,9 % |
Revue de cette décision
Section intitulée « Revue de cette décision »- 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.
Références
Section intitulée « Références »- ADR-001 — Temporal pour orchestrer la pipeline
- ADR-003 — OCR
- ADR-027 — fields DOC_KYC / SELFIE routent vers cette pipeline
- Standards : ICAO 9303 (MRZ), NIST SP 800-63A (IAL), ISO/IEC 30107-3 (PAD), iBeta L1/L2/L3
- Modèles publics : InsightFace ArcFace, Tesseract 5 + arabe
- Page engineering : Pipeline biométrique
- POC : poc-mrz-parser
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
Contexte
Section intitulée « Contexte »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 :
- 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.
- Productivité agent : un agent doit traiter 30–60 cases/jour. Une UX mal foutue (clics multiples, attentes serveur) coûte 30 % de capacité.
- 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.
Décision
Section intitulée « Décision »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 client5 décisions structurantes :
-
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 casecase_reopenlié). -
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). -
SLA dérivé Temporal — chaque case démarre un workflow Temporal
CaseSlaWorkflowqui 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-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.
-
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 tablecase_eventimmutable (pas d’UPDATE/DELETEau niveau Postgres). Conservation 10 ans WORM.
Justification
Section intitulée « Justification »- 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.
Conséquences
Section intitulée « Conséquences »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.
Décisions secondaires
Section intitulée « Décisions secondaires »- 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,commentsvia 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.
Alternatives considérées et écartées
Section intitulée « Alternatives considérées et écartées »| Option | Pourquoi é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 polling | Risque 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 externe | Risque d’oubli côté tenant ; dépendance externe. Préférable comme primitive intégrée. |
| Pas de bulk operations | Un MLRO doit pouvoir traiter en lot lors de découverte de fraude organisée. Sans bulk, il quitte l’outil. |
Métriques cibles
Section intitulée « Métriques cibles »| Metric | MVP cible | V2 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 requis | 100 % (enforcé serveur) | 100 % |
Disponibilité case-mgmt-svc | 99,5 % | 99,9 % |
| Tests automatisés couvrant state machine | 100 % des transitions | idem |
Revue de cette décision
Section intitulée « Revue de cette décision »- 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.
Références
Section intitulée « Références »- 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
Contexte
Section intitulée « Contexte »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 :
- 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. - Performance live : le screening tourne pendant l’onboarding → cible ≤ 200 ms p95 end-to-end pour ne pas dégrader le parcours client.
- 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)
Décision
Section intitulée « Décision »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 :
-
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_maxcross-fields +multi_match. Score natif uniquement comme rappel — pas comme verdict. -
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.
-
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. -
Audit log signé append-only — chaque screening émet un event
sanctions.screening.completedavec(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. -
Packaging à 2 modes —
embedded(container Docker single-node OpenSearch + tar.gz, 4-8 GB RAM, géré par VitaKYC, pour tenants light) ouexternal(cluster 3 nœuds existant chez tenant tier-1). Même image VitaKYC, déclenché par feature flag tenant.
Justification
Section intitulée « Justification »- 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.
Conséquences
Section intitulée « Conséquences »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.
Décisions secondaires
Section intitulée « Décisions secondaires »- 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.
Alternatives considérées et écartées
Section intitulée « Alternatives considérées et écartées »| Option | Pourquoi écartée |
|---|---|
| SQLite FTS5 pour listes publiques + OpenSearch pour DJ | Hybride 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 Kotlin | Ré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 edges | Marche 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 edges | Surplus pour ≤ 2 sauts. Licence GPL pour Community ou commerciale pour Enterprise. À ouvrir si > 2 sauts demandé. |
| Pas de re-ranker, OpenSearch score = verdict | Score 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). |
Métriques cibles
Section intitulée « Métriques cibles »| Metric | MVP cible | V2 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 service | 99,5 % | 99,9 % |
Revue de cette décision
Section intitulée « Revue de cette décision »- 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.
Références
Section intitulée « Références »- ADR-001 — Temporal pour orchestration workflow screening
- ADR-002 — multi-tenant
- ADR-006 — gestion des listes AML
- ADR-029 — un screening hit déclenche un case
- Listes publiques :
- Standards : Follow the Money (FtM) schema, Beider-Morse phonetic algorithm, Daitch-Mokotoff soundex
- Page engineering : Sanctions screening
- POC : poc-sanctions-matcher
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
Contexte
Section intitulée « Contexte »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 :
- 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.
- Détection multi-fenêtres :
montant cumulé > 50 000 TND sur 24 hou≥ 10 transactions structurées de < 10 000 TND sur 7 j. Demande state stores persistants par client + fenêtres glissantes parallèles. - 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
Décision
Section intitulée « Décision »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 :
-
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). -
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. -
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). -
State stores RocksDB local — chaque pod
tx-monitoring-svcmaintient 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). -
Alert dedup par signature SHA-256 —
alertId = 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. -
Outbox pattern Postgres → Kafka — l’alerte est d’abord persistée en Postgres
alert_outboxdans la même transaction que la maj de l’état. Un sweeper indépendant pousse vers Kafkaaml.alert.published. Pattern éprouvé pour exactly-once trans-frontière DB ↔ broker.
Justification
Section intitulée « Justification »- 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.
Conséquences
Section intitulée « Conséquences »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.
Décisions secondaires
Section intitulée « Décisions secondaires »- Topic
tx.normalized: entrée du moteur, partitionné paraccountId(cohérence ordering par compte). Schema Avro versionné. - Topic
aml.alert.published: sortie. Partitionné partenantId. Consommé parcase-mgmt-svc(crée case si severity ≥ MEDIUM) etnotification-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 logaml_rule_auditappend-only. - Shadow mode : nouvelle règle activée en
SHADOWpendant 14 jours — produit alertes mais enaml.alert.shadowseparate 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-svctraite des PII (montant, contreparties). Chiffrement au repos (RocksDB encrypted) + tenant key segregation.
Alternatives considérées et écartées
Section intitulée « Alternatives considérées et écartées »| Option | Pourquoi écartée |
|---|---|
| Apache Flink | Cluster séparé JobManager/TaskManager, complexité ops > bénéfices à notre échelle. À reconsidérer si > 5 M tx/jour ou besoin ML streaming complexe. |
| ksqlDB | Surcouche SQL Kafka Streams — ajoute un layer sans gain métier. Notre DSL Kotlin reste plus expressif et auditable. |
| Esper / Drools Fusion | Excellents mais commerciaux ou complexes (CEP avec syntaxe propre). Notre DSL Kotlin est plus simple à enseigner et plus auditable BCT. |
| Spring Cloud Stream + custom rules | Possible 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 rules | Performance OK, mais durabilité moindre, pas de exactly-once natif, pas de changelog compacté gratuit. |
Métriques cibles
Section intitulée « Métriques cibles »| Metric | MVP cible | V2 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-svc | 99,5 % | 99,9 % |
| Late event drop rate | ≤ 0,1 % | ≤ 0,01 % |
Revue de cette décision
Section intitulée « Revue de cette décision »- 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.
Références
Section intitulée « Références »- 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
Contexte
Section intitulée « Contexte »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.
Décision
Section intitulée « Décision »Pattern unifié webhook-svc : émetteur central + spec de signature + protocole + outbox + dashboard, consommé par tous les modules via interface WebhookEmitter.
5 décisions structurantes :
-
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). -
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. -
Outbox Postgres → HTTP — chaque module appelle
WebhookEmitter.queue(eventType, payload)qui insert danswebhook_outboxPostgres dans la même transaction. Sweeper indépendant pousse vers HTTP avec retry. Garantit at-least-once + cohérence DB ↔ delivery. -
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. -
Headers standardisés —
X-VitaKYC-Event-Id,X-VitaKYC-Event-Type,X-VitaKYC-Tenant-Id,X-VitaKYC-Delivery-Attempt,X-VitaKYC-Idempotency-Key(pour deduplication tenant-side).
Justification
Section intitulée « Justification »- 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.
Conséquences
Section intitulée « Conséquences »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
rotatequi 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.
Décisions secondaires
Section intitulée « Décisions secondaires »- Conservation outbox : 30 jours après
DELIVERED, 90 jours aprèsFAILEDfinal. 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.verdictvers/kyc-callback,aml.alertvers/compliance-callback). UI admin tenant pour gérer. - Rate limit côté tenant : si tenant retourne 429 (rate limit), on respecte
Retry-Afterheader. 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/testpermet 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.
Alternatives considérées et écartées
Section intitulée « Alternatives considérées et écartées »| Option | Pourquoi écartée |
|---|---|
| mTLS obligatoire | Friction d’intégration tenant (CA + cert lifecycle). Acceptable en option, pas en obligatoire. |
| JWT signé Ed25519 dans body | Standard moins courant que HMAC en webhooks, plus complexe côté tenant (parsing JWT vs comparaison HMAC). |
| AWS SNS / Webhook Relay payant | Coût récurrent + dépendance externe. Réinventer en interne plus économique à notre échelle. |
| Pas de retry, fire-and-forget | Inacceptable pour banque (on perd des notifications de décision case = un client client en limbo). |
| WebSockets push persistant | Demande tenant de maintenir une connexion ouverte 24/7. Irréaliste pour banques avec pare-feu. |
| gRPC streaming | Non supporté par défaut dans les stacks back-end banque (Java EE 8, .NET, etc.). |
Métriques cibles
Section intitulée « Métriques cibles »| Metric | MVP cible | V2 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-svc | 99,5 % | 99,9 % |
| Throughput | ≥ 1 000 webhooks/min | ≥ 10 000 |
Revue de cette décision
Section intitulée « Revue de cette décision »- 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
Références
Section intitulée « Références »- ADR-002 — multi-tenant RLS pour
webhook_outbox - ADR-031 — outbox pattern réutilisé
- Webhooks signés — spec engineering — détails HMAC, retry, dashboard
- POC poc-webhook-emitter — implémentation Kotlin
- Convention industrie : Stripe webhooks signature, GitHub webhooks
- Standards : RFC 8785 JSON Canonicalization, HMAC RFC 2104
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
Contexte
Section intitulée « Contexte »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 :
- Identités plateforme — agents compliance L1/L2/MLRO, DSI tenant, auditeur, dev (humans utilisant VitaKYC)
- 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.
Décision
Section intitulée « Décision »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 :
-
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)
-
Realm strategy hybride — single
vitakyc-saaspour 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. -
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.
-
RBAC + ABAC en pure Kotlin (
shared/auth-client-jvm) — typesRole,Seniority,Permission,AssuranceLevel, principalVitaKycPrincipal. Pure functionsAuthz.hasPermission,requireSeniority,requireSameTenant,requireStepUp. Réutilisable backend (Ktor middleware) + tests unitaires. -
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
-
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.exportouaudit.replaysecrets.rotate(webhooks, API keys, tenant signing keys)users.manage(création/suppression)sanctions.lists.refreshforce-refreshforms.publish
Justification
Section intitulée « Justification »- 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.
Conséquences
Section intitulée « Conséquences »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).
Décisions secondaires
Section intitulée « Décisions secondaires »- 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.v1consommé paraudit-svcqui appose signature Ed25519 + chainage hash. Append-only Postgresauth_audit_event10 ans WORM. - Anti-CSRF : tokens anti-CSRF côté UI back-office (ASP.NET-style ou OWASP CSRF guard).
- Cookies :
Secure,HttpOnly,SameSite=Strictpartout. Pas de localStorage pour les tokens (XSS risk).
Alternatives considérées et écartées
Section intitulée « Alternatives considérées et écartées »| Option | Pourquoi écartée |
|---|---|
| Auth0 / Okta SaaS | DX excellent mais : pas air-gap (no go BCT), lock-in, pricing par MAU explose à scale (~3 USD/MAU × 50 K agents = 150 K/an). |
| Authentik | Plus 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 tous | Empêche federation tier-1 (banque veut Azure AD), policy MFA spécifique tenant impossible. |
| 1 realm strict par tenant | Multiplication ops pour fintechs light qui n’ont pas d’IdP. Pattern hybride retenu. |
| OAuth Implicit Flow | Dé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 annotations | Magie 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 ≥ L2 | NIST SP 800-63B déprécié 2017, attaques SS7 documentées en TN. Refus politique. |
| Step-up MFA optionnel par tenant | Risque 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écurrent | OK onboarding initial, jamais récurrent (boîte mail compromise = tous les comptes accessibles). |
Métriques cibles
Section intitulée « Métriques cibles »| Metric | MVP cible | V2 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 rate | 100 % | 100 % |
| Disponibilité Keycloak | 99,5 % | 99,9 % |
| Token refresh latency p95 | ≤ 200 ms | ≤ 100 ms |
| Brute force lockout precision (false lockout rate) | ≤ 0,5 % | ≤ 0,1 % |
Revue de cette décision
Section intitulée « Revue de cette décision »- 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.
Références
Section intitulée « Références »- ADR-002 — multi-tenant RLS
- ADR-009 — i18n RTL pour login Keycloak
- ADR-029 — 4-eyes principle
- ADR-032 — outbox pattern audit pour events auth
- Standards : OIDC 1.0, OAuth 2.0 RFC 6749, PKCE RFC 7636, WebAuthn Level 2, NIST SP 800-63B
- Compliance : BCT Circulaire 2017-08 + 2018-07, RGPD art. 32, ANSI référentiel, ISO 27001 A.9
- Page engineering : Auth system spec A-Z
- Repo : vitakyc-platform
shared/auth-client-jvm+platform/auth-svc - Convention industrie : Stripe SCA + step-up, Auth0 step-up best practices
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
Contexte
Section intitulée « Contexte »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 TCR — Tax Compliance & Reporting — qui couvre 2 obligations :
- 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).
- 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).
Décision
Section intitulée « Décision »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 + signatures4 sous-modules :
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.).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 %.
- Classification entité :
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.).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.
Justification
Section intitulée « Justification »- 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.
Conséquences
Section intitulée « Conséquences »Positives
- Promesse produit
KYC + AML + TCRenfin 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.
Décisions secondaires
Section intitulée « Décisions secondaires »- 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(FATCAMessageType=FATCA1avecNoAccountToReport). 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.
Alternatives considérées et écartées
Section intitulée « Alternatives considérées et écartées »| Option | Pourquoi écartée |
|---|---|
| Sous-traiter à Sovos / Trans-Tax / FATCAManager | Coû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 wrapper | Possible 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 XSLT | Ajoute une étape, des bugs. XML généré directement avec un builder typé Kotlin = plus simple à auditer. |
| MessageRefId basé UUID | Casse 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-control | Risque de 6 chiffres : transmettre 100 000 comptes erronés à l’IRS = sanctions §6041 + §1471. Dual-control obligatoire. |
| Générateur CRS en Python | L’écosystème VitaKYC est Kotlin/JVM. Cohérence opération + DSL classifier partagé avec Risk Engine. |
Métriques cibles
Section intitulée « Métriques cibles »| Metric | MVP | V2 |
|---|---|---|
| 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 % |
Revue de cette décision
Section intitulée « Revue de cette décision »- 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.
Références
Section intitulée « Références »- POC FATCA generator — XML FATCA v2.0 + DGI TN
- POC CRS generator — XML CRS v2.0 OCDE
- Page engineering : Pipeline TCR
- ADR liés : ADR-001 (Temporal), ADR-005 (MinIO WORM), ADR-006 (signatures), ADR-007 (capture indicia)
- Standards : IRS Pub 5124 FATCA XML schema v2.0, OECD CRS XML schema v2.0, IRS IDES, Loi tunisienne 2016-71 (transposition CRS)
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
Contexte
Section intitulée « Contexte »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 :
- 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.
- 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.
- 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).
- 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.
Décision
Section intitulée « Décision »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 servicefun Application.module() { configureObservability(serviceName = "mrz-svc", serviceVersion = "0.1.0") // ... autres plugins}| Pilier | Outil | Format | Backend |
|---|---|---|---|
| Traces | OpenTelemetry SDK (Java) auto-instrument Ktor + manuel Temporal | OTLP gRPC | Tempo |
| Metrics | Micrometer + registry Prometheus | OpenMetrics | Prometheus scraping /metrics |
| Logs | Logback JSON encoder + MDC enrichi traceId/spanId/tenantId | JSON line | Loki (Promtail / OTLP logs |
| UI / Alerting | Grafana dashboards provisionnés + Alertmanager | — | dashboards 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 ←── alertmanagerL’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.
Conventions obligatoires
Section intitulée « Conventions obligatoires »| Convention | Règle | Exemple |
|---|---|---|
| Nom de service | <bounded-context>-svc ou <bounded-context>-jvm | mrz-svc, form-engine-jvm |
| Resource attributes (OTel) | service.name, service.version, deployment.environment, service.namespace=vitakyc | obligatoires |
| Span name HTTP server | <METHOD> <route> | POST /v1/mrz/parse |
| Metric name | snake_case + unité | vitakyc_http_server_requests_seconds, vitakyc_mrz_parse_total |
| Cardinality | tenant_id permis ; jamais d’identifiants utilisateurs ou de PII | OK : tenant_id="TN-BANQUEX" ; KO : client_id="..." |
| Sampling traces | tail-based — 100 % des erreurs + 100 % des slow > p95 + 5 % du reste | configurable Collector |
| Logs | toujours JSON, MDC : traceId, spanId, tenantId, userId, requestId | format Logback LogstashEncoder |
Exigences SLO golden signals
Section intitulée « Exigences SLO golden signals »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.
Justification
Section intitulée « Justification »- 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).
Conséquences
Section intitulée « Conséquences »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).
Décisions secondaires
Section intitulée « Décisions secondaires »- 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 dansinfra/observability/prometheus-rules.yaml.
Alternatives écartées
Section intitulée « Alternatives écartées »| Option | Pourquoi écartée |
|---|---|
| Datadog / New Relic SaaS | Coû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 gateway | Anti-pattern Prometheus pour services long-lived. Conservé uniquement pour les jobs batch (TCR, scheduled workers). |
Revue de cette décision
Section intitulée « Revue de cette décision »- Sprint 1 :
mrz-svcinstrumenté avecobservability-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-svcmesuré 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).
Références
Section intitulée « Références »- Page engineering : Observability — spec A-Z + dashboards
- ADR-001, ADR-002
- Standards : OpenTelemetry Spec, W3C Trace Context, Google SRE Book — Monitoring distributed systems, OpenMetrics, RED method, USE method
- Helm charts de référence :
grafana/loki-stack,grafana/tempo-distributed,prometheus-community/kube-prometheus-stack
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
Contexte
Section intitulée « Contexte »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 :
- 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.
- 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. - 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.
Décision
Section intitulée « Décision »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 :
| Pilier | Choix | Justification |
|---|---|---|
| Transport | HTTP POST /v1/audit/events (Ktor) en MVP, outbox local + retry exponentiel côté client | Simple, 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. |
| Persistence | PostgreSQL table append-only (RLS multi-tenant ADR-002) + index (tenant_id, ts) + GIN sur details JSONB | Simple. 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. |
| Retention | 10 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 enforcedREVOKE 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'));Catégories d’events standards
Section intitulée « Catégories d’events standards »Convention <DOMAIN>_<VERB> en SCREAMING_SNAKE :
| Domaine | Events |
|---|---|
| Auth | LOGIN_OK, LOGIN_FAIL, MFA_REQUESTED, MFA_OK, MFA_FAIL, STEP_UP_OK, LOGOUT, TOKEN_REFRESH, PASSWORD_RESET |
| Tenant | TENANT_CREATED, TENANT_SUSPENDED, TENANT_REACTIVATED, TENANT_DELETED |
| Form Designer | FORM_DRAFT_SAVED, FORM_PUBLISHED, FORM_ACTIVATED, FORM_VERSION_DELETED |
| Risk Matrix | POLICY_PUBLISHED, POLICY_ACTIVATED, POLICY_SHADOWED, POLICY_BACKTEST_RUN |
| Biometric | KYC_VERIFICATION_STARTED, KYC_VERIFICATION_COMPLETED, KYC_VERIFICATION_FAILED, LIVENESS_FAIL, FACE_MATCH_FAIL |
| Case Mgmt | CASE_CREATED, CASE_ASSIGNED, CASE_DECISION_APPROVED, CASE_DECISION_REJECTED, CASE_ESCALATED |
| AML / Sanctions | SCREENING_RUN, SCREENING_HIT_DETECTED, SCREENING_HIT_DISPOSED, LIST_UPDATED |
| Webhooks | WEBHOOK_DELIVERED, WEBHOOK_FAILED_RETRYING, WEBHOOK_DEAD_LETTERED |
| Admin | USER_CREATED, USER_DEACTIVATED, ROLE_GRANTED, ROLE_REVOKED, CONFIG_CHANGED |
| Data | DATA_EXPORTED, DSAR_REQUESTED, DSAR_FULFILLED, RETENTION_PURGED |
Tout event critique (severity ∈ {WARN, ALERT}) est aussi propagé via webhook tenant (cf ADR-032) si configuré.
Justification
Section intitulée « Justification »- 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-jvmpersiste les events dans une queue locale (mémoire bornée 10 K + spill disk H2/SQLite) et les flush async versaudit-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-tenantpeut faire des queries cross-tenant (audité lui-même).
Conséquences
Section intitulée « Conséquences »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).
Décisions secondaires
Section intitulée « Décisions secondaires »- 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_idetip_addresssont des données personnelles → soumis aux retention policies + DSAR.detailsJSONB 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 = truepour 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>_CORRECTEDqui référence l’event_idoriginal. - Trace correlation : chaque event porte son
trace_idetrequest_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 leurhmac_key_idpour permettre la vérification même après rotation.
Alternatives écartées
Section intitulée « Alternatives écartées »| Option | Pourquoi écartée |
|---|---|
| Splunk Audit / Datadog Audit Trails / AWS CloudTrail | Coût récurrent ($30K+/an pour notre volume), pas air-gap, RGPD complexe pour banque TN/FR. |
| Audit log = Kafka topic + ksqlDB | Plus complexe à opérer (Kafka cluster + Confluent Schema Registry + ksqlDB compute). MVP n’a pas le besoin. Migration possible V2. |
| Audit log = Loki structured logs | Loki 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 directly | Pas 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 tree | Sur-ingénierie. HMAC chain suffit pour le besoin (linéaire, immutable, vérifiable). Merkle tree ajoute complexité sans bénéfice ici. |
Revue de cette décision
Section intitulée « Revue de cette décision »- Sprint 2 :
audit-svcactivé, ingestion HTTP fonctionnelle, intégration depuisauth-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).
Références
Section intitulée « Références »- ADR-002, ADR-005, ADR-032, ADR-033, ADR-035
- Standards : BCT Circulaire 2017-08 LCB-FT annexe D, RGPD art. 30 et 32, SOC 2 Trust Services Criteria CC4.1, ISO 27001 A.12.4 Logging and monitoring, NIST SP 800-92
- Page engineering : Audit log centralisé
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
Contexte
Section intitulée « Contexte »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.mdligne 158,mobile-sdk-integration.md,observability.md) montrent des payloads entenant_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 entenantId,userId,createdAt. - Les DTOs Kotlin de
form-engine-jvmetform-designer-svcannotent 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 :
- 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). - 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.
- Audit forensics ambigu — corréler un event
audit_log.details.tenantIdavec un payload entranttenant_idimpose un mapping mental à chaque debug.
Décision
Section intitulée « Décision »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 :
| Couche | Convention | Mécanisme |
|---|---|---|
| Identifiants Kotlin / Java in-process | camelCase | idiomatique JVM, inchangé |
| Sérialisation JSON wire (REST, webhooks, events Kafka, JSONB) | snake_case | @SerialName("snake_case") sur chaque propriété concernée |
| Path params URL | camelCase toléré (/v1/forms/{tenantId}/{formId}) | héritage Ktor, reste lisible |
| Query params | snake_case | aligne avec body |
| Headers HTTP custom | Kebab-Case | RFC 7230 standard |
| Noms de métriques OTel | snake_case | déjà imposé par ADR-035 |
| Noms de colonnes SQL | snake_case | déjà standard PostgreSQL |
| Noms de topics Kafka | dotted.snake_case | dé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.
Justification
Section intitulée « Justification »- 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_idtraverse 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 compatibles —
kotlinx.serializationsupporte nativement@SerialName; Jackson supportePropertyNamingStrategies.SNAKE_CASEau niveau ObjectMapper si jamais on doit s’y replier.
Conséquences
Section intitulée « Conséquences »Côté code (JVM)
- Tout nouveau DTO
@Serializableexposé 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
@Serializablesans@SerialNamesur des propriétés camelCase (à ajouter en sprint 2). - Les DTOs internes non sérialisés restent en
camelCasenatif Kotlin.
Côté documentation
- Tous les exemples JSON (
*.mdengineering/, compliance/, architecture/) sont normaliséssnake_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_casetelles quelles (response.tenant_id). Pas de rewrap automatique en camelCase — on garde la cohérence wire ↔ code TS.
Path params
- L’exception
camelCaseautorisé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.
Alternatives écartées
Section intitulée « Alternatives écartées »| Option | Pourquoi écartée |
|---|---|
camelCase partout | Casse 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/query | Incohé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 annotation | kotlinx.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). |
Revue de cette décision
Section intitulée « Revue de cette décision »- 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.
Références
Section intitulée « Références »- ADR-035 — Observability (snake_case déjà imposé pour métriques)
- ADR-036 — Audit log centralisé (colonnes
audit_eventen snake_case) - Standards externes : Stripe API conventions, Google JSON Style Guide, JSON:API spec.
ADRs à produire ultérieurement (backlog)
Section intitulée « ADRs à produire ultérieurement (backlog) »- 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.