1. Pourquoi avez‑vous besoin d’audit & lifecycle dans ChatGPT App
Tant que vous écrivez un prototype, l’utilisateur — c’est vous, la base de données — un SQLite local, et les « incidents » se soignent par git reset --hard, tout semble simple et confortable.
Mais dès que votre GiftGenius (ou une autre application ChatGPT (App)) reçoit de vrais utilisateurs, surtout avec paiements et PII, apparaissent soudain :
- les équipes sécurité du client avec la question « qui peut voir nos commandes et qui les a modifiées ? »;
- les juristes avec la question « combien de temps conservez‑vous les données et comment exécutez‑vous la demande “supprimez‑moi” ? »;
- la réalité de production avec la question « que se passe‑t‑il si un développeur supprime une table en prod ? ».
Dans ce cours, nous allons examiner quatre blocs clés :
- Journaux d’audit — une couche de journalisation distincte pour la sécurité et l’audit.
- Data retention — la durée de vie des différents types de données et comment l’implémenter.
- Suppression sur demande par l’utilisateur — « droit à l’oubli » dans la technique.
- Business continuity & backups — comment survivre aux pannes sans perdre la face ni les données.
Autant que possible, nous rattacherons les exemples à notre App pédagogique (GiftGenius sur Next.js + Apps SDK + MCP).
2. Journaux d’audit : qui, quoi, quand et avec quel résultat
En quoi les journaux d’audit diffèrent‑ils des logs applicatifs
Les logs applicatifs habituels sont des messages conviviaux pour les développeurs. On y trouve des stack traces, des informations de debug, des valeurs étranges de variables, des console.log("ici, il ne devrait certainement pas y avoir null") de mise au point. Ils vivent peu de temps et sont lus par des ingénieurs.
Les journaux d’audit — c’est un autre univers. Leur public principal — les équipes sécurité, les auditeurs, parfois des juristes. Ils n’ont pas besoin de la ligne « NullPointer à la ligne 55 », ils ont besoin d’un enregistrement du type « l’utilisateur X a modifié les paramètres de paiement de l’organisation Y à telle heure, résultat — succès ». Les enregistrements d’audit vivent généralement beaucoup plus longtemps (des années) et sont considérés comme des preuves en cas d’enquête.
Différences clés :
| Caractéristique | Application Logs | Audit Logs |
|---|---|---|
| Objectif | Débogage, diagnostic | Sécurité, conformité, enquêtes |
| Audience | Développeurs, SRE | Équipes sécurité, juristes, parfois régulateurs |
| Contenu des données | Détails techniques, stack trace | Qui/quoi/quand/quel ressource/avec quel résultat |
| Durée de conservation | Semaines–mois | Années (souvent ≥ 1 an) |
| Opérations sur les logs | Peuvent être supprimés/réécrits | Idéalement append‑only, sans UPDATE/DELETE |
OWASP et des guides similaires soulignent séparément : il est préférable de conserver les journaux d’audit dans un stockage ou une table distincte, plutôt que de les mélanger avec les logs applicatifs.
Que journaliser dans le contexte d’une application ChatGPT
Pour une application ChatGPT, surtout avec de la monétisation, un minimum raisonnable d’audit serait :
- événements d’authentification : connexion, déconnexion, tentatives de connexion ;
- opérations sur des données critiques : création/mise à jour/suppression de profils, commandes, paramètres de paiement ;
- actions administratives : changement de rôles, modification des paramètres du tenant ;
- appels d’outils sensibles MCP/Agents : create_order, charge_customer, cancel_subscription, etc.
Bonne heuristique : tout ce que vous voudriez demander lors d’un incident du style « qui a fait ça et par quel biais ? » doit partir à l’audit.
Structure d’un événement d’audit
Un modèle mental pratique : chaque enregistrement — « qui / quelle action / sur quoi / dans quel contexte / avec quel résultat ». On reformule souvent cela comme une structure who, action, resource, context, outcome.
Pour notre GiftGenius, décrivons l’interface en TypeScript :
// lib/audit.ts
export type AuditAction =
| "auth.login"
| "auth.logout"
| "order.create"
| "order.cancel"
| "account.delete"
| "giftidea.generate";
export interface AuditEvent {
eventId: string; // uuid
timestamp: string; // ISO
actor: {
userId: string | null; // peut être null avant la connexion
tenantId?: string | null;
ip?: string | null;
client: "chatgpt-app" | "admin-panel" | string;
};
action: AuditAction;
resource?: {
type: string; // "order", "user", ...
id?: string;
};
context?: {
mcpTool?: string;
requestId?: string;
};
outcome: {
status: "success" | "failure";
reason?: string | null;
};
}
Notez que l’événement n’inclut pas d’e‑mail complet, de numéros de carte et autres PII, que, d’après les cours précédents, nous avons appris à masquer et à ne pas journaliser sans nécessité.
Où et comment stocker les journaux d’audit
Exigences minimales pour le stockage :
- une table distincte ou même une base distincte des logs applicatifs, pour réduire le risque d’écrasement accidentel ;
- si possible en mode append‑only : techniquement, cela peut être une simple politique « nous ne faisons jamais de UPDATE/DELETE sur cette table », plus un rôle BD n’ayant que les droits INSERT et SELECT ;
- accès restreint : toute l’équipe d’ingénierie n’a pas besoin de lire l’audit complet.
Si vous utilisez PostgreSQL via Prisma/Drizzle, le modèle peut ressembler à ceci (exemple simplifié) :
CREATE TABLE audit_events (
event_id uuid PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now(),
actor_user_id text,
actor_tenant_id text,
actor_ip inet,
action text NOT NULL,
resource_type text,
resource_id text,
context_mcp_tool text,
context_request_id text,
outcome_status text NOT NULL,
outcome_reason text
);
Le schéma s’adapte à vos besoins, mais l’essentiel — c’est la structuration. Vous maudirez plus tard les blobs JSON sur une seule ligne.
Implémentation de l’audit dans notre App
Faisons un petit helper dans l’application Next.js (environnement Node, par exemple dans un serveur MCP ou une route API) :
// lib/audit.ts
import { randomUUID } from "crypto";
import { db } from "./db"; // votre client de BD
export async function logAudit(event: Omit<AuditEvent, "eventId" | "timestamp">) {
const full: AuditEvent = {
...event,
eventId: randomUUID(),
timestamp: new Date().toISOString(),
};
// En production — via une file/tâche en arrière-plan ; ici, simple insertion
await db.insertInto("audit_events").values({
event_id: full.eventId,
created_at: full.timestamp,
actor_user_id: full.actor.userId,
action: full.action,
outcome_status: full.outcome.status,
outcome_reason: full.outcome.reason ?? null,
// ...autres champs
});
}
Ajoutons maintenant l’appel dans le handler qui crée une commande (supposons que ce soit un outil MCP ou un endpoint serveur) :
// app/api/orders/route.ts
export async function POST(req: Request) {
const user = await requireUser(req); // depuis le module d’authentification
const body = await req.json();
const order = await createOrderInDb(user, body);
await logAudit({
actor: { userId: user.id, client: "chatgpt-app" },
action: "order.create",
resource: { type: "order", id: order.id },
context: { mcpTool: "create_order_tool" },
outcome: { status: "success" },
});
return Response.json(order);
}
Même chose autour des opérations risquées — annulation de commande, modification des coordonnées de paiement, suppression de compte.
Nous avons obtenu une couche d’audit séparée et structurée — parfait. La question suivante se pose naturellement : combien de temps tous ces événements (et le reste des données utilisateur) doivent‑ils vivre et que faire une fois le délai écoulé ?
3. Data retention : combien de temps vivent vos données
Pourquoi ne pas tout conserver indéfiniment
L’instinct d’ingénieur « au cas où » est très dangereux pour les données utilisateur.
Premièrement, plus vous gardez de données longtemps, plus les conséquences d’une fuite sont lourdes : plus le baril d’essence est gros, plus l’incendie est grave. De nombreux guides de protection des données qualifient directement les données « d’actif toxique » : il faut les conserver utilement, mais en minimisant volume et durée.
Deuxièmement, la législation de type GDPR/CCPA introduit le principe « pas plus longtemps que nécessaire à la finalité du traitement ». Autrement dit, on ne peut pas garder indéfiniment des données personnelles « au cas où ». Pour chaque type de données, des durées de conservation claires et des procédures de suppression ou d’anonymisation doivent être définies.
Troisièmement, le stockage cloud coûte de l’argent. Les grandes tables de logs et d’historique de conversations grossissent vite, et au bout d’un an on découvre que la moitié de la facture du fournisseur — ce sont des « déchets d’hier ».
Des données différentes — des durées différentes
L’expérience des entreprises et les guides publics donnent à peu près ce tableau :
| Type de données | Durées de conservation typiques |
|---|---|
| Logs de debug, métriques techniques | de 1 à 12 mois |
| Journaux d’audit | ≥ 12 mois, parfois 2–5 ans |
| Commandes, paiements, factures | 3–7 ans (exigences comptables/fiscales) |
| Sessions, jetons temporaires | heures–jours |
| Conversations brutes / requêtes | de quelques semaines à quelques mois, voire pas de conservation |
| Agrégats anonymisés (analytique) | plus longtemps, car il n’y a plus de PII |
Important : ce n’est pas un conseil juridique, mais des repères techniques. Pour un produit réel, vous validerez les durées avec les juristes, mais techniquement vous devez déjà être prêts à implémenter des TTL différents.
Comment implémenter la rétention dans le code
Le motif le plus courant : la table possède created_at ou expires_at, et vous avez un processus périodique qui supprime ou anonymise les enregistrements anciens.
Exemple : nettoyage des logs applicatifs de plus de 90 jours.
// scripts/cleanup-logs.ts
import { db } from "../lib/db";
async function cleanup() {
await db
.deleteFrom("app_logs")
.where("created_at", "<", new Date(Date.now() - 90 * 24 * 60 * 60 * 1000));
console.log("Old logs removed");
}
cleanup().catch(console.error);
Ce script peut être exécuté par cron, via GitHub Actions selon un planning, ou à l’aide d’un ordonnanceur cloud.
Pour les PII, on préfère souvent l’anonymisation à la suppression. Par exemple, les commandes de plus de N années perdent le lien avec l’utilisateur concret :
UPDATE orders
SET user_id = NULL
WHERE created_at < now() - interval '3 years';
Les montants, les produits et autres « comptabilités » sont conservés, mais le lien avec la personne disparaît.
N’oubliez pas que les sauvegardes ont elles aussi leurs propres durées de vie. Nous discuterons de la périodicité et de la durée de conservation des backups séparément dans le bloc sur les sauvegardes, mais l’idée reste la même : même les archives ne doivent pas être conservées indéfiniment, sinon le « droit à l’oubli » devient fictif.
4. Suppression sur demande de l’utilisateur : « droit à l’oubli » dans le code
D’où vient l’exigence
Le GDPR européen (et des lois similaires) introduit le « droit à l’oubli » : un utilisateur peut demander la suppression de ses données personnelles, et l’entreprise doit l’effectuer sans retard injustifié.
Du point de vue du développeur, cela signifie : tôt ou tard, vous recevrez une demande « supprimez toutes mes données » (ou vous proposerez vous‑même un bouton « Delete my data »), et il faudra non seulement supprimer la ligne dans la table users, mais aussi suivre toute la piste : commandes, sessions, jetons, journaux d’actions, CRM, paiements, etc.
Mais il existe aussi des lois qui vous obligent à conserver certaines données : les transactions financières, par exemple. Il y a donc plus de complexité juridique que technique ici.
Que faut‑il exactement purger
Jeu minimal pour notre GiftGenius :
- profil utilisateur (nom, e‑mail, paramètres) ;
- sessions, refresh tokens, liens avec des fournisseurs OAuth ;
- commandes, si elles ne sont pas nécessaires sous forme « personnalisée » (ou si on peut les anonymiser) ;
- logs et journaux d’audit contenant des PII (par exemple, un e‑mail en clair).
Restent des données importantes pour le reporting, mais déjà anonymisées — montants des commandes, nombre de transactions, agrégats par pays, etc.
Exemple d’algorithme de suppression
Schéma du scénario :
- L’utilisateur (authentifié) clique sur « Supprimer mon compte ».
- Le serveur reçoit une requête avec son userId.
- Le serveur :
- supprime/anonymise les enregistrements dépendants (commandes, sessions, intégrations) ;
- purge les PII du profil ;
- écrit une entrée dans le journal d’audit « demande de suppression de données traitée ».
Pour simplifier, montrons une variante minimale sur deux tables. Dans un produit réel, autour de ce noyau vous ajouterez d’autres entités (intégrations, services tiers, etc.).
Code de service dans Next.js (exemple simplifié) :
// app/api/delete-me/route.ts
import { db } from "@/lib/db";
import { logAudit } from "@/lib/audit";
export async function POST(req: Request) {
const user = await requireUser(req);
await db.transaction(async (tx) => {
await tx.deleteFrom("sessions").where("user_id", "=", user.id);
await tx.deleteFrom("orders").where("user_id", "=", user.id);
await tx.updateTable("users")
.set({
is_deleted: true,
name: null,
email: null,
})
.where("id", "=", user.id);
await logAudit({
actor: { userId: user.id, client: "chatgpt-app" },
action: "account.delete",
outcome: { status: "success" },
});
});
return new Response(null, { status: 204 });
}
Dans le monde réel, vous ajouterez ici des appels d’API externes (par exemple Stripe — pour détacher le customer), et vous rendrez la transaction plus robuste. Mais le principe est là : tout au même endroit, avec une entrée d’audit.
Lien avec les sauvegardes
La partie délicate « et les backups alors ? » soulève de nombreuses questions intéressantes. Même si vous avez supprimé l’utilisateur de la base de prod, ses données peuvent rester dans les snapshots nocturnes. Pour éviter que cela ne devienne « en réalité, on ne supprime jamais personne », deux approches existent :
- Les sauvegardes ont elles‑mêmes une durée de vie limitée (par exemple 30–90 jours) et disparaissent ensuite avec les données. Après expiration du retention, ni la base principale ni les archives ne contiennent l’utilisateur.
- Si vous remontez malgré tout un système depuis une sauvegarde, vous avez un registre des ID « supprimés » et, après restauration, vous rejouez les scripts de suppression/anonymisation.
Dans les grandes entreprises, on pratique parfois le crypto‑shredding : les PII de l’utilisateur sont chiffrées avec une clé distincte, et lors de la demande de suppression, c’est la clé qui est détruite. Même si des copies chiffrées restent quelque part (dans des logs, des backups), sans la clé, ce n’est que du déchet inutile. C’est très bien, mais un peu « aérospatial » pour une startup.
Point UX important
Souvenez‑vous que la suppression — ce n’est pas que du SQL. L’utilisateur s’attend à :
- un moyen clair d’envoyer la demande (bouton, formulaire, e‑mail) ;
- des délais raisonnables d’exécution (en pratique jusqu’à 30 jours) ;
- une notification de succès ou un refus motivé (par exemple, lorsque certaines données doivent être conservées par la loi).
Techniquement, vous êtes déjà prêts : vous savez purger, journaliser l’action et ne pas conserver inutilement dans les sauvegardes.
5. Business continuity & sauvegarde
Imaginez maintenant que tout ce qui précède fonctionne parfaitement… jusqu’à un fatal DROP TABLE sur orders, une panne cloud ou la chute d’une région. Nous avons besoin de mécanismes pour ramener le service à la vie dans un délai raisonnable et ne pas perdre de données critiques.
RTO et RPO — deux acronymes qui définissent votre douleur
Deux paramètres de base du Disaster Recovery :
- RTO (Recovery Time Objective) — combien de temps vous pouvez vous permettre d’être indisponibles. Par exemple, si RTO = 1 heure, cela signifie qu’après une panne sérieuse, vous devez remettre le système en ligne en une heure maximum.
- RPO (Recovery Point Objective) — combien de données, en temps, vous êtes prêts à perdre. Si RPO = 10 minutes, cela signifie qu’à la restauration, vous pouvez perdre les 10 dernières minutes d’historique, mais pas plus.
Plus le produit est critique (banque, systèmes de trading), plus ces deux paramètres tendent vers zéro. Pour le GiftGenius pédagogique, on peut vivre avec un RTO de quelques heures et un RPO de 15–60 minutes, mais il faut tout de même l’implémenter.
Ce qui peut mal tourner dans votre stack
Dans le contexte d’une application ChatGPT sur Vercel + BD cloud + API externes, la liste typique des soucis est la suivante :
- OpenAI API indisponible : votre App renvoie des erreurs sur les tool‑calls.
- Vercel (ou autre) en panne : le widget ne peut pas joindre votre backend.
- Base de données corrompue ou quelque chose a été supprimé par erreur (par exemple, DROP TABLE).
- Compte compromis ou cassé pilotant l’infrastructure.
Vous répondez à tout cela par une combinaison de sauvegardes, de réplications et d’un comportement raisonnable de l’application en cas de panne.
Stratégies de sauvegarde
Les Postgres/BD cloud modernes proposent généralement au moins trois options :
- Sauvegardes complètes + incrémentales.
Réaliser un snapshot complet de la BD une fois par jour et conserver les changements incrémentaux entre deux snapshots. La restauration consiste à revenir au snapshot cible puis à rejouer le journal des changements. - Point‑in‑Time Recovery (PITR).
La base écrit un journal des transactions (WAL) et permet de se restaurer à un instant donné (par exemple « état à 14:03:00, avant que nous ayons supprimé la table »). - Réplication vers une autre région.
Maintenir une réplique passive ou active de la BD dans une autre région/un autre cloud. En cas de perte de la région principale, on bascule l’application sur la réplique, en ne perdant que les données qui n’étaient pas encore répliquées.
À notre échelle, il suffit généralement d’activer le PITR chez le fournisseur de BD et des backups périodiques off‑site.
Exemple simple : dump quotidien pour une BD locale/dev
Même si, en production, vous vous reposez sur une BD managée, pour staging/dev il est parfois utile d’avoir un script simple :
# scripts/backup.sh
#!/usr/bin/env bash
set -e
DATE=$(date +%F)
pg_dump "$DATABASE_URL" > "backups/backup-$DATE.sql"
echo "Backup created: backups/backup-$DATE.sql"
On peut l’exécuter via cron ou GitHub Actions. L’essentiel — ne pas oublier que les backups doivent aussi être supprimés à l’échéance.
Comportement de l’App en cas de panne de services externes
Les sauvegardes et le PITR résolvent le problème « que faire si tout est cassé ou si les données sont corrompues ». Mais, dans la réalité métier, on voit plus souvent des pannes partielles — une API externe est indisponible, le réseau est coupé, le prestataire de paiement est bloqué.
Quand l’API d’OpenAI ou le prestataire de paiement est à terre, la pire stratégie — tomber en 500 brut avec un stack trace inutile. Idéalement :
- le backend renvoie une erreur structurée du type { error: "upstream_unavailable" } ;
- le widget affiche un message compréhensible : « Service temporairement indisponible, réessayez plus tard » ;
- le système n’inonde pas l’API en panne de retries infinis (les patterns Circuit Breaker, etc., seront vus plus en détail dans le module résilience).
Exemple de handler MCP‑tool qui prend en compte une erreur externe :
// mcp/tools/createGiftIdea.ts
export async function createGiftIdea(args: Input): Promise<Output> {
try {
return await callOpenAiModel(args);
} catch (err) {
await logAudit({
actor: { userId: args.userId ?? null, client: "chatgpt-app" },
action: "giftidea.generate",
outcome: { status: "failure", reason: "openai_unavailable" },
});
throw new Error("UPSTREAM_UNAVAILABLE");
}
}
Ensuite, votre couche entre MCP et le widget sait déjà comment afficher proprement cette erreur dans l’UI.
Vérification de restauration : une sauvegarde sans restore — juste un fichier
L’anti‑pattern classique : on fait un backup quotidien, tout le monde est content… jusqu’à découvrir qu’ils sont impossibles à restaurer (format changé, clé perdue, manque d’espace).
Plan minimal :
- remonter périodiquement (par exemple une fois par mois) un environnement staging depuis une sauvegarde ;
- parcourir les scénarios de base : connexion, création de commande, fonctionnement de l’App ;
- vérifier que le temps de restauration et la perte de données respectent vos RTO/RPO.
Sans faire un cours de « religion DevOps », retenez dans ce cours : les processus de sauvegarde font partie de l’architecture de l’App, et non « quelque chose que quelqu’un fera quelque part dans le cloud ».
6. Visualisation : cycle de vie des données et des événements
Pour que tout cela ne soit pas qu’en mots, dessinons deux schémas simples.
Cycle de vie des données utilisateur
flowchart TD
A["Création de données<br/>(inscription, commande)"] --> B["Stockage et utilisation<br/>(prod DB)"]
B --> C["Archivage/agrégation<br/>(métriques anonymisées)"]
B --> D[Demande de suppression]
D --> E[Suppression/anonymisation<br/>dans la prod DB]
E --> F["Expiration des sauvegardes<br/>(retention)"]
Idée principale : le cycle de vie des données ne s’arrête pas à la base de prod — il se prolonge dans les sauvegardes.
Flux d’audit pour une action dangereuse
sequenceDiagram
participant User as Utilisateur
participant ChatGPT as ChatGPT
participant App as Votre backend/MCP
participant DB as Base de données
participant Audit as Stockage d’audit
User->>ChatGPT: "Annule la commande #123"
ChatGPT->>App: callTool cancel_order
App->>DB: UPDATE orders SET status='canceled'
App->>Audit: INSERT audit_event {actor, action, resource, outcome}
App-->>ChatGPT: Résultat de l'opération
ChatGPT-->>User: Message de résultat
7. Erreurs courantes dans audit & lifecycle
Erreur n° 1 : mélanger journaux d’audit et logs applicatifs.
Quand tous les messages atterrissent dans un même index logs, au bout de six mois, plus personne ne distingue « l’utilisateur a modifié le rôle administrateur » de « nous avons encore une null reference ». L’audit doit contenir des événements structurés de niveau métier (voir la section sur la structure d’un événement d’audit) et un stockage séparé avec accès limité.
Erreur n° 2 : journaliser des PII dans l’audit et les logs de debug.
E‑mail complet, téléphone, adresse de livraison, quatre derniers chiffres de carte — tout cela se retrouve souvent par inadvertance dans les logs. Cela accroît le risque de fuite et contredit les recommandations de confidentialité. À la place, journalisez des identifiants et des valeurs masquées.
Erreur n° 3 : absence de politique de rétention — « on garde tout pour toujours ».
Au stade MVP, cela paraît « acceptable », et après un an, vos tables atteignent des tailles monstrueuses, et toute requête analytique se transforme en DDoS de la base. De plus, vous violez le principe de minimisation inscrit dans les lois modernes sur les données. Des TTL minimaux par type de données doivent être pensés, et le nettoyage — automatisé.
Erreur n° 4 : « Suppression sur demande » == DELETE FROM users.
Si vous supprimez simplement la ligne utilisateur mais laissez ses PII dans les commandes, sessions et logs, vous n’avez en réalité supprimé personne. L’approche correcte — parcourir transactionnellement toutes les entités liées, et là où l’on ne peut pas supprimer — anonymiser. Sans oublier d’enregistrer le fait de la suppression comme événement d’audit.
Erreur n° 5 : ignorer les sauvegardes lors de la suppression des données.
Vous avez supprimé l’utilisateur en prod — bien, mais ses données vivent encore un an dans de vieux snapshots. À la restauration, tout « ressuscite », et vous violez à nouveau vos engagements envers l’utilisateur et votre Privacy Policy. Il faut soit limiter la durée de vie des sauvegardes, soit disposer d’une procédure de réapplication des suppressions après restauration.
Erreur n° 6 : « nous avons activé les sauvegardes, donc tout va bien », mais personne n’a essayé de restaurer.
Un backup jamais testé en restore — ce n’est qu’un fichier coûteux. Sans vérification périodique de restauration, vous ne connaissez ni le RTO/RPO effectifs, ni si votre plan de DR fonctionne. Minimum — relever régulièrement un staging depuis une sauvegarde avec un check‑list.
Erreur n° 7 : décalage entre documentation et réalité.
Dans la Privacy Policy, vous écrivez que vous conservez les logs 30 jours et que vous supprimez les données sur demande, mais dans le code, tout reste à jamais. La boutique ChatGPT, les clients entreprise et les auditeurs le verront facilement avec des questions comme « montrez la table de retention » et « démontrez la suppression d’un utilisateur concret ». Mieux vaut d’abord faire, puis écrire.
GO TO FULL VERSION