Chapitre 14.1 — Schéma de données & minimisation
⏱️ TL;DR — La conformité commence dans le
schema.prisma, pas dans la politique de confidentialité. Quatre réflexes de conception : (1) ne modéliser que le strictement nécessaire (chaque colonne se justifie par une finalité) ; (2) séparer les données sensibles (santé, art. 9) dans une table dédiée à accès restreint, plutôt que de les noyer dansusers; (3) pseudonymiser — unuserIdopaque relie les usages sans traîner le nom partout ; (4) chiffrer au repos les quelques champs vraiment sensibles (le comment est en Partie 9). Et le piège numéro un : le champnotesfourre-tout en texte libre, qui finit par contenir tout et n’importe quoi, y compris des données que tu n’as jamais eu le droit de collecter. C’est la traduction directe de la protection des données dès la conception (art. 25).
🎯 Objectifs
- Transformer le principe de minimisation (art. 5) et de privacy by design (art. 25) en décisions de modélisation.
- Séparer les identités, les usages et les données sensibles en tables distinctes.
- Pseudonymiser dès le schéma avec un identifiant opaque.
- Repérer et éliminer les anti-patterns : sur-collecte, champ
noteslibre, sensible en clair.
Le schéma est une décision RGPD
L’article 25 impose de penser la protection des données dès la conception (by design) et de ne traiter, par défaut (by default), que les données nécessaires à chaque finalité. Pour un dev, ça ne se joue pas en réunion : ça se joue au moment où tu écris la table. Une colonne qu’on ajoute « au cas où » est une dette de conformité — il faudra un jour la justifier, la sécuriser, la purger, l’exporter et l’effacer.
La question à se poser pour chaque champ : quelle finalité précise le justifie ? S’il n’y a pas de réponse claire, le champ ne doit pas exister.
💡 Réflexe — Le schéma par défaut n’est pas « on met tout dans
User, on triera plus tard ». C’est l’inverse : on part de rien, et chaque colonne doit gagner sa place en pointant une finalité. Modéliser, c’est déjà minimiser.
Avant / après : un modèle Prisma qui sur-collecte
Voici le modèle typique d’un début de projet FormaCampus, écrit sans réflexe RGPD. Il « marche », mais il est une bombe à retardement.
// ❌ AVANT — tout dans une seule table, sur-collecte, fourre-tout
model User {
id Int @id @default(autoincrement())
email String @unique
passwordHash String
nom String
prenom String
dateNaissance DateTime // utile ? pour quelle finalité ?
telephone String? // jamais utilisé par l'app
adresse String? // idem
handicap String? // ⚠️ donnée de SANTÉ (art. 9) en clair, dans users
numeroSecu String? // ⚠️ pourquoi ??? aucune finalité
notes String? // 🕳️ le trou noir : texte libre "fourre-tout"
ipDerniereConnexion String? // PII conservée sans limite
createdAt DateTime @default(now())
}Les problèmes, un par un :
dateNaissance,telephone,adresse: collectés « par habitude », sans finalité active côté application. Sur-collecte (art. 5, minimisation).numeroSecu: aucune finalité légitime dans un LMS. À supprimer, point.handicap: c’est une donnée de santé (catégorie particulière, art. 9). Elle n’a rien à faire en clair dans la tableusers, accessible à tout le code qui lit un profil.notes: le champ fourre-tout. En pratique, le support y écrit « parent séparé, joindre la mère », « suit un traitement », « conflit avec Mme X »… soit des données sensibles et non prévues, impossibles à cartographier, à sécuriser ou à purger.ipDerniereConnexion: une donnée personnelle (chapitre 1.1) gardée sans durée ni finalité claire.
⚠️ Piège — Le champ
notesen texte libre est l’anti-pattern RGPD le plus courant et le plus sous-estimé. Comme il accepte tout, il finit par contenir des données de santé, des jugements de valeur, des informations sur des tiers — collectés sans base légale, sans information, sans possibilité de traçage. La CNIL sanctionne régulièrement des zones de commentaires libres au contenu inapproprié. Si tu dois garder des notes, structure-les (champs typés, valeurs contraintes) et encadre-les (consigne écrite : pas de données sensibles, pas d’appréciation subjective).
Le même schéma, repensé
On applique les quatre réflexes : minimiser, séparer, pseudonymiser, chiffrer le sensible.
// ✅ APRÈS — minimisé, séparé, pseudonymisé
// 1) Identité : le strict nécessaire à l'authentification et au contact
model User {
id String @id @default(cuid()) // identifiant OPAQUE (pseudonyme)
email String @unique
passwordHash String
prenom String
nom String
createdAt DateTime @default(now())
profile Profile?
healthData HealthRecord?
}
// 2) Données d'usage / de profil : séparées de l'identité, référencées par userId
model Profile {
userId String @id
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
etablissement String? // finalité : rattachement pédagogique
classe String? // finalité : organisation des cours
// pas de dateNaissance, pas de téléphone, pas d'adresse : aucune finalité active
}
// 3) Données SENSIBLES (santé, art. 9) : table dédiée, accès restreint, chiffré au repos
model HealthRecord {
userId String @id
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
amenagementEnc Bytes // "aménagement de handicap" CHIFFRÉ (voir Partie 9)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// aucune colonne "notes" libre ici non plus
}Ce que le schéma raconte maintenant :
User.idest uncuid()— un identifiant opaque, non devinable, non séquentiel. Toutes les autres tables (résultats, logs applicatifs, événements) référencent ceuserId, pas le nom ni l’e-mail. C’est de la pseudonymisation : les usages sont reliés à un pseudonyme, l’identité vit dans une seule table.HealthRecordest isolée : une table à part signifie qu’on peut lui coller des droits d’accès distincts (seuls le médecin scolaire ou un rôle « santé » y accèdent — voir le contrôle d’accès en 9.3), et que le reste du code qui affiche un profil n’a jamais les données de santé sous la main.amenagementEncest unByteschiffré applicativement : on ne stocke pas la valeur en clair. On ne redétaille pas comment chiffrer ici — c’est le chapitre 9.2 — mais le schéma matérialise la décision.- Le champ
notesa disparu. S’il faut vraiment tracer un besoin, on crée des champs structurés et bornés plutôt qu’un texte libre.
En SQL brut, la même séparation :
-- Identité (accès applicatif général)
CREATE TABLE users (
id TEXT PRIMARY KEY, -- identifiant opaque (pseudonyme)
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
prenom TEXT NOT NULL,
nom TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Données de santé : table séparée, chiffrées, accès restreint par les droits DB
CREATE TABLE health_records (
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
amenagement_enc BYTEA NOT NULL, -- valeur chiffrée applicativement
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Exemple : révoquer l'accès de l'app générale à la table sensible,
-- et ne l'accorder qu'à un rôle dédié.
REVOKE ALL ON health_records FROM app_readonly;
GRANT SELECT, INSERT, UPDATE ON health_records TO role_sante;Le tableau de décision « ce champ, je le modélise comment ? »
| Type de donnée | Exemple | Décision de modélisation |
|---|---|---|
| Nécessaire à une finalité claire | email, classe | Colonne typée, dans la table adéquate. |
| Sans finalité active | numeroSecu, telephone inutilisé | Ne pas modéliser. On supprime le champ. |
| Sensible (art. 9 : santé…) | aménagement de handicap | Table séparée, chiffrée, accès restreint. |
| Identifiant reliant les usages | clé étrangère vers l’utilisateur | userId opaque (cuid/uuid), jamais le nom. |
| Texte libre « au cas où » | notes | À bannir ou à structurer en champs bornés. |
| Statistique agrégée | taux de réussite d’un module | Calcul à la volée ou table de stats sans identifiant. |
🧭 Sur FormaCampus — La table
usersd’origine mélangeait identité, contact, santé et unnotesexplosif. Après refonte :User(identité minimale,idencuid),Profile(rattachement pédagogique),HealthRecord(aménagement chiffré, accès réservé au rôle santé). Résultat concret : quand un développeur code l’écran « liste des élèves d’une classe », il ne peut pas afficher par accident une donnée de santé — elle n’est ni dansUser, ni dansProfile. La conformité devient une propriété du schéma, pas une vigilance de chaque requête.
🔒 Côté personne concernée — Un parent qui confie que son enfant a un aménagement s’attend à ce que cette information soit vue uniquement par les personnes qui en ont besoin (l’enseignant référent, le médecin scolaire), pas par le stagiaire du support ni par n’importe quel script d’export. Séparer et chiffrer, ce n’est pas de la paranoïa d’ingénieur : c’est tenir la promesse implicite faite à cette famille.
📚 Le texte — La protection des données dès la conception et par défaut est à l’article 25 du RGPD ; la minimisation au 5.1.c. Les données de santé relèvent des catégories particulières de l’article 9 (interdiction de principe sauf exception). La pseudonymisation et le chiffrement sont cités comme mesures appropriées à l’article 32 (détaillés en Partie 9).
✏️ Exercices
Exercice 1 — Refais le modèle. On te donne cette table pour un formulaire d’inscription à un atelier ponctuel : Participant { id, email, prenom, nom, dateNaissance, adresse, employeur, regimeAlimentaire, commentaireLibre }. La finalité unique : organiser l’atelier (liste d’émargement + pause déjeuner). Réécris le modèle minimisé et justifie chaque suppression.
✅ Solution
Modèle minimisé : Participant { id, email, prenom, nom } — plus, si la pause déjeuner l’exige vraiment, un champ structuré regimeAlimentaire en enum (STANDARD, VEGETARIEN, SANS_GLUTEN…) et non en texte libre. On supprime : dateNaissance et adresse (aucune finalité pour un atelier ponctuel), employeur (sans objet, sauf facturation à l’employeur — alors on le justifie et on le documente). Le regimeAlimentaire en texte libre pourrait révéler une conviction religieuse (art. 9) ou une donnée de santé (allergie) : en enum borné, on limite l’exposition. commentaireLibre : supprimé — c’est le fourre-tout. Le id est un identifiant opaque.
Exercice 2 — Où mettre la donnée sensible ? Un formateur veut noter, pour certains stagiaires, une « situation de handicap nécessitant un aménagement d’examen ». Trois options te sont proposées : (a) une colonne handicap dans User ; (b) une valeur dans le champ notes ; (c) une table Accommodation séparée. Choisis et explique.
✅ Solution
(c) une table Accommodation (ou HealthRecord) séparée. C’est une donnée de santé (art. 9) : elle doit être isolée (pour lui appliquer des droits d’accès stricts, réservés au rôle qui en a besoin), chiffrée au repos, et jamais chargée par le code générique de profil. (a) l’expose à tout le code qui lit User ; (b) le notes libre est le pire choix — non structuré, non traçable, non sécurisable, et il invite à écrire bien plus que nécessaire. Bonus : vérifier qu’on a une base légale et une information claires pour ce traitement sensible (Parties 4 et 5).
🧠 Quiz de révision
1. Que dit concrètement l’article 25 pour un dev ?
Protection des données dès la conception et par défaut. Traduit : on pense minimisation, séparation et sécurité au moment de modéliser le schéma, et l’application ne traite par défaut que les données nécessaires à chaque finalité. Le bon endroit pour être conforme, c’est le schema.prisma, pas la politique de confidentialité écrite après coup.
2. Pourquoi séparer les données de santé dans une table dédiée ?
Parce que ce sont des données sensibles (art. 9) : les isoler permet de leur appliquer des droits d’accès distincts (seul un rôle habilité les lit), de les chiffrer spécifiquement, et d’éviter que le code générique qui affiche un profil ne les expose. La séparation transforme la confidentialité en propriété du schéma.
3. Qu’est-ce qu’un identifiant « opaque » et pourquoi l’utiliser ?
Un identifiant non signifiant et non séquentiel (cuid, uuid) qui sert de clé de référence pour toutes les tables d’usage, à la place du nom ou de l’e-mail. Il pseudonymise : les données d’usage sont reliées à un pseudonyme, l’identité reste concentrée dans une seule table, ce qui réduit l’exposition en cas de fuite d’une table « travail ».
4. Pourquoi le champ notes en texte libre est-il un piège ?
notes en texte libre est-il un piège ?Parce qu’il accepte tout : il finit par contenir des données sensibles (santé, situation familiale), des jugements, des informations sur des tiers — collectés sans base légale ni information, impossibles à cartographier, sécuriser ou purger. À bannir, ou à remplacer par des champs structurés et bornés (enums, valeurs contraintes) avec une consigne claire.
5. Comment décider si un champ mérite d’exister ?
En lui exigeant une finalité précise et active. La question : quelle finalité justifie ce champ ? Pas de réponse claire = pas de champ. On ne part pas de « mettons tout, on triera » mais de rien, et chaque colonne doit gagner sa place. C’est la minimisation appliquée à la modélisation.
Chapitre suivant : Coder la rétention & l’effacement — traduire « limitation de la conservation » en dates d’expiration, tâches de purge et effacement en cascade (relations, index, caches, sauvegardes, exports).