Skip to Content

Chapitre 12.5 — Architecture d’un produit interopérable

⏱️ TL;DR — On assemble tout le cours en un seul produit : une appli Next.js qui porte trois casquettes en même temps — outil LTI (Parties 9-10) qui se branche dans les LMS et renvoie les notes, émetteur xAPI (Parties 6-7) vers un LRS, et exporteur de contenu SCORM/cmi5 pour les clients qui préfèrent un paquet. Les décisions d’architecture qui comptent : multi-tenant (une seule appli, plusieurs plateformes clientes identifiées par iss + client_id + deployment_id) ; sécurité (clé privée jamais exposée, JWKS pour la clé publique, jetons courts) ; hébergement UE + RGPD (les données d’apprentissage sont des données personnelles — voir le cours RGPD) ; et stockage des données d’apprentissage (le LRS d’un côté, la config de l’autre). C’est le studio FormaCampus, de bout en bout.

🎯 Objectifs

  • Concevoir une appli Next.js qui est simultanément outil LTI, émetteur xAPI et exporteur SCORM/cmi5.
  • Rendre le produit multi-tenant : servir plusieurs plateformes clientes depuis un seul déploiement.
  • Sécuriser les clés (privée/publique), exposer un JWKS, gérer les jetons.
  • Héberger dans l’UE et traiter les données d’apprentissage en conformité RGPD.
  • Séparer proprement config produit et données d’apprentissage (LRS).

Un produit, trois casquettes

Un produit e-learning moderne n’est pas « une appli SCORM » ou « un outil LTI » : c’est une seule base de code qui expose plusieurs surfaces d’interopérabilité, selon comment le client veut la consommer.

  • Casquette LTI : le client branche l’appli dans son LMS ; l’élève arrive connecté, l’appli renvoie les notes (AGS) et lit le roster (NRPS). C’est le mode « produit vivant, hébergé ».
  • Casquette xAPI : chaque interaction pédagogique émet des statements vers un LRS, pour un suivi fin, du mobile, du présentiel. C’est le mode « données d’apprentissage riches ».
  • Casquette exporteur : pour les clients qui veulent un paquet à importer, l’appli exporte le même contenu en SCORM ou cmi5. C’est le mode « produit livré ».

La même logique pédagogique (les exercices, la correction, la notation) alimente les trois — on ne réécrit pas le contenu par surface, on le branche différemment (le principe « séparer contenu et packaging » du chapitre 12.4).

Le schéma d’architecture

Lis-le en trois flux : entrant (le LMS lance l’appli en LTI, signature validée via JWKS), sortant temps réel (l’appli émet du xAPI vers le LRS et renvoie les notes au LMS), sortant paquet (l’appli exporte du SCORM/cmi5 téléchargeable).

Multi-tenant : une appli, plusieurs plateformes

Un outil LTI qui réussit sert plusieurs LMS clients (des dizaines d’académies, d’écoles, d’entreprises) depuis un seul déploiement. Chaque plateforme cliente est un tenant. La clé : identifier sans ambiguïté de quel tenant vient chaque lancement.

En LTI 1.3, un tenant est identifié par le triplet iss (l’émetteur, l’URL de la plateforme) + client_id + deployment_id (chapitre 9.5). Tu tiens un registre qui, pour chaque tenant, stocke les endpoints de la plateforme et son JWKS :

{ "iss": "https://moodle.ecole.fr", "client_id": "10000000000042", "deployment_id": "1", "auth_login_url": "https://moodle.ecole.fr/mod/lti/auth.php", "auth_token_url": "https://moodle.ecole.fr/mod/lti/token.php", "key_set_url": "https://moodle.ecole.fr/mod/lti/certs.php" }

À chaque lancement, tu lis iss/client_id/deployment_id dans la requête, tu retrouves le tenant dans le registre, et tu valides le JWT contre son key_set_url. Un lancement dont le triplet n’est pas enregistré est refusé.

⚠️ Piège — Mélanger les tenants. Si tu valides un JWT venant du tenant A contre le JWKS du tenant B, ou si tu renvoies une note du tenant A dans le carnet du tenant B, c’est une fuite inter-clients — grave en EdTech (données d’élèves). Chaque requête doit être rattachée à son tenant dès la première ligne, et toute donnée (notes, statements, config) cloisonnée par tenant. Le triplet iss/client_id/deployment_id est la clé de cloisonnement.

💡 Réflexe — Rends le tenant explicite dans toute ta logique : la fonction qui renvoie une note prend le tenant en paramètre, la requête au LRS filtre par tenant, l’export porte l’identité du tenant. « De quel client s’agit-il ? » ne doit jamais être implicite. En multi-tenant, l’isolation est une propriété qu’on construit, pas qu’on espère.

Sécurité : clés, JWKS, jetons

LTI 1.3 repose sur de la cryptographie asymétrique (Partie 9). Ton outil a une paire de clés : la privée (qui signe) et la publique (que les plateformes utilisent pour vérifier). Les règles non négociables :

  • La clé privée ne quitte jamais le serveur. Elle vit dans un gestionnaire de secrets, jamais dans le code, jamais dans un dépôt Git, jamais côté client. Une clé privée fuitée = ton outil est usurpable.
  • Tu exposes la clé publique via un endpoint JWKS. Les plateformes le lisent pour vérifier tes JWT (par exemple lors des appels de service). Format standard :
{ "keys": [ { "kty": "RSA", "kid": "2026-key-1", "use": "sig", "alg": "RS256", "n": "0vx7ag...(modulus)...", "e": "AQAB" } ] }
  • Tu valides toujours la signature des JWT entrants contre le JWKS de la plateforme (récupéré depuis son key_set_url, mis en cache, rafraîchi car les plateformes font tourner leurs clés). Ne jamais accepter un id_token sans vérifier sa signature, son iss, son aud, son exp, son nonce.
  • Les jetons d’accès (AGS, NRPS) sont courts et ciblés. Pour appeler un service Advantage, tu obtiens un access_token via un client credentials grant (tu présentes un JWT signé par ta clé privée), avec les scopes minimaux nécessaires, et il expire vite. On ne stocke pas un jeton d’accès longtemps ; on en redemande un.

🔌 Côté intégration — Le kid (key id) dans le JWKS n’est pas décoratif : il permet la rotation des clés. Tu peux publier deux clés (l’ancienne et la nouvelle) le temps que les plateformes rafraîchissent leur cache, puis retirer l’ancienne. Sans kid et sans rotation, changer une clé compromise casse tous les lancements d’un coup. La rotation propre est un critère de certification et de sérieux vu par les intégrateurs.

⚠️ Piège — « Ça marche donc c’est sûr ». Un JWT non validé (signature ignorée) « marche » aussi — jusqu’à ce qu’un attaquant forge un lancement et se fasse passer pour un enseignant. La validation de signature via JWKS n’est pas optionnelle : c’est ce qui distingue un outil LTI conforme et sûr d’une porte ouverte. Le chapitre 9.3 l’a montré côté flux ; en architecture, c’est une règle de vie ou de mort.

Hébergement UE et RGPD

Les données d’apprentissage sont des données personnelles : un statement xAPI relie un apprenant identifiable à une activité, une note AGS pointe un élève nommé. Dès qu’il y a des élèves européens, le RGPD s’applique — et en EdTech scolaire, il s’applique souvent à des mineurs, donc avec une vigilance renforcée. Les décisions d’architecture :

  • Héberger dans l’UE : le LRS, la base de config, les logs — tout ce qui contient des données personnelles — sur une infrastructure européenne, pour éviter les transferts hors UE non maîtrisés.
  • Minimiser : ne stocke que le nécessaire. Côté xAPI, préfère un identifiant pseudonyme (account avec le sub de la plateforme) plutôt qu’un e-mail en clair (mbox) quand tu peux — le sub du lancement LTI est justement un identifiant stable et non-e-mail (chapitre 10.3).
  • Rôle de sous-traitant : quand tu héberges les données d’apprentissage des élèves d’une école, tu es sous-traitant (au sens RGPD) de cette école, responsable de traitement. Ça se contractualise (accord de sous-traitance, durées de conservation, sécurité, effacement sur demande).
  • Cloisonner par tenant : l’isolation multi-tenant (ci-dessus) est aussi une exigence RGPD — les données de l’école A ne doivent jamais être visibles par l’école B.

📚 La spec — Ici la « spec » n’est pas ADL ni 1EdTech mais le RGPD. L’interop produit la donnée personnelle (le statement, la note) ; le RGPD régit ce que tu en fais. Les deux mondes se rencontrent exactement dans le LRS et la base de notes. Le cours RGPD traite le détail (bases légales, minimisation, sous-traitance, droits des personnes, cas des mineurs) ; retiens ici que l’architecture interop et l’architecture RGPD sont le même schéma, vu sous deux angles.

🧭 Sur FormaCampus — Le studio de FormaCampus, c’est ce schéma, déployé une fois, servant toutes ses écoles. Une seule appli Next.js : côté LTI, chaque académie est un tenant (iss/client_id/deployment_id) validé contre son JWKS ; côté xAPI, chaque exercice émet des statements vers le LRS hébergé en France ; côté AGS, les notes remontent dans le carnet Moodle de chaque école, cloisonnées par tenant ; côté exporteur, le même contenu part en SCORM/cmi5 pour les clients qui préfèrent un paquet. La clé privée LTI dort dans un gestionnaire de secrets, la publique est servie via un endpoint JWKS, et les clés tournent avec un kid daté. FormaCampus est sous-traitant de chaque école, avec accord de sous-traitance et conservation bornée. Un produit, trois casquettes, N tenants, zéro fuite : c’est ça, être le dev EdTech qu’on s’arrache.

Séparer config et données d’apprentissage

Dernier principe d’architecture, souvent négligé : ne mélange pas la configuration produit et les données d’apprentissage.

Type de donnéeOù ça vitExemples
Config produitBase applicativeRegistre des tenants, clés, catalogue d’exercices, barèmes
Données d’apprentissageLRS (et cache de notes)Statements xAPI, résultats, progression, temps passé

Le LRS est le magasin dédié aux données d’apprentissage (Partie 6) : il est fait pour les recevoir, les servir, les requêter. Ta base applicative gère le reste. Cette séparation te donne : des analytics propres (tout le suivi au même endroit, requêtable), une conformité plus simple (les données personnelles d’apprentissage sont localisées, donc plus faciles à borner et à effacer), et une évolutivité (tu changes de LRS sans toucher à ta config).

💡 Réflexe — Quand tu conçois, pose-toi pour chaque donnée : « est-ce de la config, ou une trace d’apprentissage ? ». La config va dans ta base, la trace va dans le LRS. Mélanger les deux (des statements dans la base applicative, des barèmes dans le LRS) rend les analytics bancales et la conformité RGPD illisible. La bonne frontière se dessine dès le premier schéma.

✏️ Exercices

Exercice 1 — Les trois casquettes. Un client école veut un studio d’exercices intégré à son Moodle qui renvoie les notes ; un client entreprise veut le même contenu à importer dans son LMS RH sous forme de paquet ; l’équipe pédagogique veut analyser le temps passé par écran. Décris comment une seule appli sert les trois.

✅ Solution

Une base de code Next.js, trois surfaces : (1) casquette LTI pour l’école — le studio se branche dans Moodle en LTI 1.3, l’élève arrive connecté, les notes remontent via AGS ; (2) casquette exporteur pour l’entreprise — le même contenu pédagogique est exporté en SCORM/cmi5 et livré comme paquet importable dans le LMS RH ; (3) casquette xAPI pour l’analyse — chaque écran émet des statements vers le LRS, que l’équipe pédagogique requête pour le temps passé. Le cœur pédagogique (exercices, correction, notation) est unique ; seules les surfaces de sortie diffèrent. On ne réécrit pas le contenu trois fois : on le branche trois fois.

Exercice 2 — Cloisonnement multi-tenant. Ton outil sert deux académies. Un bug fait qu’une note d’un élève de l’académie A apparaît dans le carnet de l’académie B. Où est la faute d’architecture, et quelle règle l’aurait évitée ?

✅ Solution

La faute : le tenant n’était pas rattaché à la donnée de bout en bout — quelque part (résolution de l’endpoint AGS, sélection du carnet), le code a utilisé un contexte implicite ou partagé au lieu du tenant du lancement. Règle qui l’évite : identifier le tenant par iss/client_id/deployment_id dès la première ligne du lancement, et passer ce tenant en paramètre de toute opération (renvoi de note, requête LRS, export). L’isolation multi-tenant se construit explicitement ; elle est aussi une exigence RGPD (les données de A ne doivent jamais être visibles par B).

Exercice 3 — Où va la donnée ? Classe chacune en « base applicative (config) » ou « LRS (donnée d’apprentissage) » : (a) le barème d’un exercice ; (b) « Camille a terminé le module Fractions » ; (c) le key_set_url d’une académie ; (d) le temps passé par un élève sur l’écran 3 ; (e) la clé privée LTI.

✅ Solution

Config (base applicative) : (a) le barème, (c) le key_set_url du tenant, (e) la clé privée (dans un gestionnaire de secrets, pas en clair). LRS (donnée d’apprentissage) : (b) le statement de complétion, (d) le temps passé par écran. La règle : ce qui paramètre le produit va dans la base ; ce qui trace un apprenant va dans le LRS. Cette frontière rend les analytics propres et la conformité RGPD lisible (les données personnelles d’apprentissage sont localisées).

🧠 Quiz de révision

1. Quelles sont les trois casquettes d’un produit interopérable ?

Outil LTI (branché dans le LMS, renvoie les notes via AGS), émetteur xAPI (statements vers un LRS pour un suivi riche), et exporteur SCORM/cmi5 (paquet livrable). Une seule base de code, trois surfaces de sortie, alimentées par le même cœur pédagogique.

2. Comment identifie-t-on un tenant en LTI 1.3 ?

Par le triplet iss (émetteur/URL de la plateforme) + client_id + deployment_id. On l’utilise pour retrouver la config du tenant (dont son JWKS), valider le lancement et cloisonner toutes ses données. Un triplet non enregistré = lancement refusé.

3. Où vivent la clé privée et la clé publique de l’outil ?

La clé privée ne quitte jamais le serveur (gestionnaire de secrets ; jamais dans le code ni Git ni côté client) — elle signe. La clé publique est exposée via un endpoint JWKS que les plateformes lisent pour vérifier les JWT. Un kid daté permet la rotation sans casser les lancements.

4. Pourquoi les données d’apprentissage relèvent-elles du RGPD ?

Parce qu’un statement xAPI ou une note AGS relie un apprenant identifiable à une activité — c’est une donnée personnelle, souvent d’un mineur en contexte scolaire. D’où : hébergement UE, minimisation (identifiant pseudonyme plutôt qu’e-mail), rôle de sous-traitant contractualisé, et cloisonnement par tenant.

5. Pourquoi séparer config produit et données d’apprentissage ?

Parce que ce sont deux natures distinctes : la config (tenants, clés, barèmes) va dans la base applicative, les traces (statements, résultats, temps passé) vont dans le LRS dédié. La séparation donne des analytics propres, une conformité RGPD plus simple (données personnelles localisées et effaçables) et une évolutivité (changer de LRS sans toucher à la config).


Partie suivante : Cookbook & Annexes — cheatsheets (modèle de données SCORM, squelette de statement xAPI, paramètres de lancement cmi5, claims LTI 1.3), modèles réutilisables, glossaire et quiz final.

Last updated on