Client Profile Schema (CPS) — contrat canonique partagé
Module :
profile-schema-svc(micro-service léger, source de vérité du contrat). Voir ADR-026 — Intégration Form Designer ↔ Risk Matrix via CPS, ADR-007 (Form Designer), ADR-025 (Risk Matrix).
Le Client Profile Schema (CPS) est le contrat canonique qui déclare, pour chaque tenant, les variables observables par les modules VitaKYC — typiquement client.profession, client.country, session.channel, screening.ofacMatch. Il est produit par les sources (Form Designer, connecteurs, modules) et consommé principalement par le Risk Matrix.
Ce document spécifie sa structure, son cycle de vie, son API, ses contraintes de cohérence, et les événements cross-module qu’il pilote.
1. Problème résolu
Section intitulée « 1. Problème résolu »Sans CPS, deux modes de défaillance silencieuse classiques :
| Sans CPS | Scénario | Conséquence |
|---|---|---|
| Champ fantôme | Risk Matrix référence client.homeowner_status qui n’existe nulle part | Règle passe la validation syntaxique mais ne matche jamais. Zéro true positive. Détectable seulement via audit approfondi. |
| Règle orpheline | Form Designer supprime le champ profession utilisé par 3 règles | Règles deviennent muettes en silence. Perte de détection PEP et professions à risque. |
Avec CPS :
- Le Form Designer déclare ce qu’il collecte → CPS enregistre avec origine + model card
- Le Risk Matrix editor autocomplete depuis le CPS, refus serveur si symbole inexistant
- Form Designer refuse la suppression d’un champ référencé par policy
ACTIVE/SHADOW - Notifications cross-module à chaque changement structurant
2. Modèle de données
Section intitulée « 2. Modèle de données »2.1 Structure
Section intitulée « 2.1 Structure »interface ClientProfileSchema { tenantId: string; // "TN-BANQUEX" version: string; // ISO-8601 timestamp de dernière modification hash: string; // SHA-256 du contenu pour intégrité variables: Variable[];}
interface Variable { /** Chemin dot-notation canonique, ex : "client.profession" */ path: string;
/** Type de la variable */ type: "string" | "integer" | "decimal" | "boolean" | "enum" | "iso3166-alpha2" | "iso4217" // country code, currency | "date" | "timestamp" | "email" | "phone";
/** Si type=enum, nom du catalogue de valeurs autorisées */ enumCatalog?: string; // ex: "BCT_PROFESSION_CODES"
/** Module ou source qui émet cette variable */ source: string; // "form:FORM_KYC_INDIVIDUAL@v2.7" | "screening:worldcheck" | ...
/** Obligatoire à l'évaluation ? (sinon fallback défini dans policy) */ required: boolean;
/** Classification sensibilité */ sensitivity: "PUBLIC" | "INTERNAL" | "PII" | "SPI"; // SPI = Sensitive PII
/** Field deprecated (bientôt retiré) */ deprecated: boolean; deprecatedAt?: string; // ISO-8601 deprecatedReason?: string;
/** Liste des consommateurs (références back) */ usedBy: UsedByRef[];
/** Model card locale au champ (YAML inline pour audit) */ modelCard?: string;}
interface UsedByRef { kind: "risk.rule" | "risk.override" | "aml.rule" | "report.field"; ref: string; // "policy:TN-BANQUEX@2.1#dimension=client#rule=3"}2.2 Exemple complet
Section intitulée « 2.2 Exemple complet »{ "tenant_id": "TN-BANQUEX", "version": "2026-04-23T11:42:00Z", "hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "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" }, { "kind": "aml.rule", "ref": "txmon-policy:TN-BANQUEX@1.0#rule=cash_intensive_profession" } ] }, { "path": "client.country", "type": "iso3166-alpha2", "source": "form:FORM_KYC_INDIVIDUAL@v2.7", "required": true, "sensitivity": "PII", "deprecated": false, "used_by": [ { "kind": "risk.rule", "ref": "policy:TN-BANQUEX@2.1#dimension=geo" }, { "kind": "risk.override", "ref": "policy:TN-BANQUEX@2.1#mustProhibit=2" } ] }, { "path": "screening.ofacMatch", "type": "enum", "enum_catalog": "SCREENING_OUTCOME", "source": "screening:worldcheck+opensanctions", "required": false, "sensitivity": "INTERNAL", "deprecated": false, "used_by": [ { "kind": "risk.override", "ref": "policy:TN-BANQUEX@2.1#mustProhibit=1" } ] }, { "path": "client.homeowner_status", "type": "boolean", "source": "form:FORM_KYC_INDIVIDUAL@v2.6", "required": false, "sensitivity": "PII", "deprecated": true, "deprecated_at": "2026-03-15T00:00:00Z", "deprecated_reason": "Remplacé par client.housing_type (plus granulaire). Retrait prévu 2026-06-15.", "used_by": [] } ]}3. Architecture
Section intitulée « 3. Architecture »3.1 Choix techniques
Section intitulée « 3.1 Choix techniques »| Composant | Choix | Rationale |
|---|---|---|
| Service | Kotlin Spring Boot | Cohérent avec le stack VitaKYC |
| Stockage | PostgreSQL JSONB append-only | Versioning naturel, requêtes riches, compatible on-prem Oracle via couche d’abstraction |
| Messaging | Kafka topics cps.source-update.{tenantId} (in) + cps.updated.{tenantId} (out) | Réutilise infra existante, garantit livraison, ordering par tenant |
| Cache consommateur | Caffeine TTL 30s + invalidation sur event Kafka | Lecture locale hot path Risk Matrix, pas de DB hit par évaluation |
| Validation | JSON Schema 2020-12 (version minimale) + extensions custom | Standard, outillé, bibliothèque Kotlin stable |
| API | REST + JSON | Simple, cacheable via CDN interne, compatible dev tools |
| Sécurité | mTLS interne + Row-Level Security PostgreSQL | ADR-002 |
4. Cycle de vie
Section intitulée « 4. Cycle de vie »4.1 Scénario nominal : nouveau champ
Section intitulée « 4.1 Scénario nominal : nouveau champ »4.2 Scénario protégé : suppression d’un champ utilisé
Section intitulée « 4.2 Scénario protégé : suppression d’un champ utilisé »4.3 Scénario bloqué : règle référant un symbole inexistant
Section intitulée « 4.3 Scénario bloqué : règle référant un symbole inexistant »POST /v1/risk/policies/{id}/publish { rules: [ { score: 80, predicate: "client.homeowner_status > 0" } ] }
→ HTTP 400{ "error": "CPS_SYMBOL_UNKNOWN", "message": "La variable 'client.homeowner_status' n'existe pas dans le CPS du tenant TN-BANQUEX.", "suggestions": ["client.housing_type", "client.home_owner"], "hint": "Un champ similaire a été déprécié le 2026-03-15. Utilisez client.housing_type."}4.4 États d’une variable
Section intitulée « 4.4 États d’une variable »- ACTIVE : lisible, proposable, référençable
- DEPRECATED : toujours lisible mais plus proposé dans autocomplete, warning dans Risk Matrix editor
- RETIRED : plus lisible, rupture — mais uniquement après fenêtre + vérif consommateurs
5. API REST
Section intitulée « 5. API REST »5.1 Lire le schéma courant d’un tenant
Section intitulée « 5.1 Lire le schéma courant d’un tenant »GET /v1/cps/tenant/TN-BANQUEXAuthorization: Bearer <service-token>
→ 200 OK{ "tenant_id": "TN-BANQUEX", "version": "2026-04-23T11:42:00Z", "hash": "sha256:e3b0...", "variables": [ ... ]}5.2 Lire une variable spécifique
Section intitulée « 5.2 Lire une variable spécifique »GET /v1/cps/tenant/TN-BANQUEX/variable/client.profession
→ 200 OK{ "path": "client.profession", "type": "enum", "enum_catalog": "BCT_PROFESSION_CODES", "source": "form:FORM_KYC_INDIVIDUAL@v2.7", "deprecated": false, "used_by": [ ... ]}5.3 Lire l’historique d’une variable
Section intitulée « 5.3 Lire l’historique d’une variable »GET /v1/cps/tenant/TN-BANQUEX/variable/client.profession/history
→ 200 OK{ "path": "client.profession", "timeline": [ { "version": "2026-01-15T...", "change": "created", "source": "form:FORM_KYC_INDIVIDUAL@v2.5" }, { "version": "2026-03-10T...", "change": "type_changed", "from": "string", "to": "enum" }, { "version": "2026-04-23T...", "change": "used_by_added", "ref": "policy:..." } ]}5.4 Vérifier l’existence d’un symbole (batch)
Section intitulée « 5.4 Vérifier l’existence d’un symbole (batch) »POST /v1/cps/tenant/TN-BANQUEX/validate-symbols{ "symbols": ["client.profession", "client.foo_bar"] }
→ 200 OK{ "valid": ["client.profession"], "invalid": [ { "symbol": "client.foo_bar", "suggestions": ["client.foo_type", "client.bar_flag"] } ]}5.5 Deprecate / undeprecate (admin uniquement)
Section intitulée « 5.5 Deprecate / undeprecate (admin uniquement) »PATCH /v1/cps/tenant/TN-BANQUEX/variable/client.homeowner_statusAuthorization: Bearer <admin-token>
{ "deprecated": true, "deprecated_reason": "Remplacé par client.housing_type", "window_days": 90 }
→ 200 OK5.6 Source declare (service → service, interne)
Section intitulée « 5.6 Source declare (service → service, interne) »POST /v1/cps/internal/declareAuthorization: mTLS client certX-Source-Service: form-designer
{ "tenant_id": "TN-BANQUEX", "source": "form:FORM_KYC_INDIVIDUAL@v2.7", "variables": [ { "path": "client.profession", "type": "enum", "enum_catalog": "BCT_PROFESSION_CODES", ... } ]}
→ 202 Accepted (traité de manière asynchrone)6. Evénements Kafka
Section intitulée « 6. Evénements Kafka »6.1 Événements consommés (cps.source-update.{tenantId})
Section intitulée « 6.1 Événements consommés (cps.source-update.{tenantId}) »| Event | Émetteur | Payload clé |
|---|---|---|
form.published | Form Designer | formId, version, addedFields[], removedFields[], renamedFields[] |
screening.enabled | Module screening | providerName, variablesDeclared[] |
rne.connected | Connecteur RNE | variablesDeclared[] (ex: entity.ubo.anyPep) |
txmon.enabled | AML TxMon | variablesDeclared[] (ex: transactional.velocity_30d) |
product-catalog.updated | Backend banque | productCodes[], variablesDeclared[] |
6.2 Événements émis (cps.updated.{tenantId})
Section intitulée « 6.2 Événements émis (cps.updated.{tenantId}) »| Event | Payload |
|---|---|
cps.variable.added | path, type, source, sensitivity |
cps.variable.deprecated | path, deprecatedAt, windowDays, reason |
cps.variable.retired | path, retiredAt |
cps.variable.used_by_changed | path, addedConsumers[], removedConsumers[] |
cps.schema.published | version, hash, addedCount, deprecatedCount, retiredCount |
Les consommateurs (Risk Matrix editor, AML TxMon editor, reporting) écoutent ces events pour invalider leur cache et notifier leurs users.
7. Règles métier invariantes
Section intitulée « 7. Règles métier invariantes »| Règle | Enforcement |
|---|---|
Un chemin (path) est unique dans le schéma d’un tenant | Contrainte unique PostgreSQL |
sensitivity=PII ou SPI → jamais loggué en clair | Intercepteur Logback + regex masking |
| Pas de cross-tenant reference | Row-Level Security PostgreSQL, tenant_id propagé en MDC |
| Deprecate → window 90j minimum avant retirement | Validation applicative |
Retirement → interdit si usedBy non vide | Validation applicative |
Rename (path change) | Non autorisé : crée new_path + deprecate old_path — l’admin doit migrer manuellement les consommateurs |
| Type change | Autorisé uniquement si backward-compatible (ex: enum → enum plus large). Sinon : nouveau path + deprecate. |
| Source unique par variable | Une variable a une seule source émettrice (pas de race condition) |
8. Performance & scalabilité
Section intitulée « 8. Performance & scalabilité »| Métrique | Target | Comment atteint |
|---|---|---|
GET /v1/cps/tenant/{id} p99 | ≤ 10 ms | Cache L1 Caffeine côté consommateur + CDN interne |
| Propagation source → consommateur | ≤ 2 s | Kafka + invalidation Caffeine async |
| Taille CPS / tenant | ~ 50 KB typique, max 500 KB | Petit contrat, peu de variables (30-150 typique) |
| Throughput writes | 10 req/s par tenant | Les sources ne publient pas à haute vélocité (form publish, rare) |
| Stockage PostgreSQL | < 1 GB / 100 tenants / 5 ans | JSONB compressé + partitionnement par tenant |
9. Sécurité & privacy
Section intitulée « 9. Sécurité & privacy »- Pas de PII dans le CPS : seulement des métadonnées (nom de variable, type, source, sensibilité). Jamais de valeurs.
- mTLS interne entre les services (déclaration + lecture privilégiée)
- RBAC côté API publique :
cps.read: tous les services du tenantcps.admin: compliance officer senior + DSIcps.deprecate: compliance officer senior + DSIcps.source.declare: service-to-service (mTLS cert whitelist)
- Row-Level Security : tenant_id propagé systématiquement
- Audit WORM : chaque changement (ajout, deprecate, retire) archivé 10 ans avec hash chain SHA-256
- Intégrité : chaque version du schéma a un
hashSHA-256 + chaîne avec version précédente → vérification au chargement par chaque consommateur
10. Intégration avec les modules
Section intitulée « 10. Intégration avec les modules »10.1 Form Designer
Section intitulée « 10.1 Form Designer »- Déclare les champs à la publication (
form.publishedevent) - Affiche le badge “Utilisé par N règles de risque” en consultant
GET /v1/cps/tenant/{id}/variable/{path} - Refuse la suppression d’un champ avec
usedBy.length > 0 - Propose la déprécation en cliquant “Deprecate 90j”
10.2 Risk Matrix
Section intitulée « 10.2 Risk Matrix »- Consomme le CPS au chargement de l’éditeur de policy (cache L1)
- Autocomplete les chemins à partir du CPS (
client.,screening., etc.) - Valide à la sauvegarde / publication : refus serveur HTTP 400 si symbole inexistant
- Affiche “source originelle : form:FORM_KYC_INDIVIDUAL@v2.7” sur chaque règle
- Écoute
cps.variable.deprecated→ warning dans policies utilisatrices - Bloque publication d’une policy référant un champ
RETIRED
10.3 AML TxMon (futur)
Section intitulée « 10.3 AML TxMon (futur) »- Déclare les variables transactionnelles (
transactional.velocity_30d,transactional.cash_intensity) - Consomme comme Risk Matrix pour ses règles temps réel
10.4 Reporting
Section intitulée « 10.4 Reporting »- Utilise le CPS comme field picker dans les builders de rapports
- Attention sensitivity : masque automatiquement
PII/SPIsauf rôle spécifique
11. Migration des tenants existants
Section intitulée « 11. Migration des tenants existants »Pour un tenant déjà en prod sans CPS, migration en 3 étapes :
- Discovery — inspecte les policies Risk Matrix existantes et les forms Form Designer, produit un CPS initial.
- Validation manuelle — compliance officer et admin produit valident le CPS reconstitué (fenêtre 30j).
- Go-live — active les validations strictes (refus HTTP 400 si symbole inexistant). Rollback possible pendant 90j.
12. Checklist go-live
Section intitulée « 12. Checklist go-live »-
profile-schema-svcdéployé (staging + prod) - Kafka topics créés par tenant (
cps.source-update.{id},cps.updated.{id}) - Form Designer wire events
form.published - Risk Matrix UI wire autocomplete depuis CPS
- Risk Matrix API wire validation stricte
- Cache Caffeine configuré côté consommateurs
- Audit WORM configuré (retention 10 ans)
- Monitoring : latence p99 API, lag Kafka, drift schéma
- Alertes : symbole inexistant référencé, tentative retire avec consommateurs
- Training compliance officers (1h) : concept CPS + workflow dépréciation
- Training admin tenants (30 min) : mapping CPS obligatoire sur Form Designer
- Exercise incident : suppression champ utilisé → vérification blocage + workflow correct
- Export audit pour BCT : export du CPS au format signé JSON
13. Références
Section intitulée « 13. Références »- ADR-026 — Intégration Form Designer ↔ Risk Matrix via CPS
- ADR-007 — Form Designer
- ADR-025 — Risk Matrix
- Risk Engine — architecture
- POC Kotlin — CPS Registry
- POC Kotlin — Risk Engine
- Maquettes UI workflow 3 · Form Designer (badge “Used by X rules”)
- Maquettes UI workflow 10 · Risk Matrix (autocomplete CPS)
- JSON Schema 2020-12
- Patterns industrie : Onfido Applicant Profile, Persona Inquiry Template, Sumsub User Level