GDPR Article 5(1)(e) requires that personal data not be kept longer than necessary for the processing purpose — the storage limitation principle. CCPA §1798.100 creates parallel expectations. AI conversation history is one of the richest stores of personal data an application can accumulate: it contains exactly what users said, in their own words, about their problems, intentions, and circumstances. Without an automated retention policy, this data accumulates indefinitely, increasing the blast radius of any future breach and the scope of any regulatory discovery request. ISO/IEC 27001:2022 A.8.10 (Information Deletion) codifies this as a formal control requirement.
High because indefinitely retained AI conversation data violates GDPR Art. 5(1)(e)'s storage limitation principle and ISO 27001 A.8.10, creating compounding regulatory and breach exposure with every day the data grows.
Set up an automated cleanup on a 30–90 day cycle. For Supabase/PostgreSQL, pg_cron is the lowest-friction option:
-- Run in Supabase SQL editor after enabling pg_cron extension
SELECT cron.schedule(
'delete-old-conversations',
'0 2 * * 0',
$$DELETE FROM conversations WHERE created_at < NOW() - INTERVAL '90 days'$$
);
For Vercel deployments, use a cron route:
// app/api/cron/cleanup-conversations/route.ts
export async function GET(req: Request) {
if (req.headers.get('Authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Unauthorized', { status: 401 })
}
const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)
await db.conversation.deleteMany({ where: { createdAt: { lt: cutoff } } })
return Response.json({ ok: true })
}
Document the retention period in your privacy policy. Choose a period appropriate to your product — 30 days for ephemeral support chats, 90 days for productivity tools where users reference past sessions.
ID: ai-data-privacy.data-retention-deletion.chat-history-retention-policy
Severity: high
What to look for: Enumerate every relevant item. Look for automated cleanup mechanisms for AI conversation storage. Signals: TTL/expiry configuration on the database collection or table storing conversations (e.g., MongoDB TTL index, Redis EXPIRE commands, Supabase pg_cron extension with a cleanup query); a cron job or scheduled function that deletes records older than a defined threshold (look in vercel.json for crons config, app/api/cron/ route handlers, or scheduled job configuration); or a Prisma schema with a deletedAt / expiresAt field with corresponding cleanup logic.
Pass criteria: At least 1 of the following conditions is met. An automated retention mechanism exists — either a database-level TTL, a scheduled cleanup job, or an explicit expiry column with cleanup logic that runs regularly. The retention period is defined (any defined period is acceptable).
Fail criteria: Conversation/message records are stored with no TTL, no expiry field, and no scheduled cleanup job. Data appears to accumulate indefinitely.
Skip (N/A) when: No conversation history is persisted — the application is stateless, using only the current session context without database storage of message history.
Detail on fail: "AI conversation history stored in [table/collection] with no TTL, expiry field, or scheduled cleanup — records accumulate indefinitely"
Remediation: Indefinitely stored AI conversations increase privacy risk in proportion to their age. A 30–90 day default retention period is common for chat applications.
For Supabase/PostgreSQL, set up a scheduled cleanup with pg_cron:
-- Enable pg_cron extension (Supabase Dashboard > Extensions)
-- Then create a weekly cleanup job:
SELECT cron.schedule(
'delete-old-conversations',
'0 2 * * 0', -- Every Sunday at 2am UTC
$$DELETE FROM conversations WHERE created_at < NOW() - INTERVAL '90 days'$$
);
For Vercel cron jobs:
// vercel.json
{
"crons": [
{ "path": "/api/cron/cleanup-conversations", "schedule": "0 2 * * 0" }
]
}
// app/api/cron/cleanup-conversations/route.ts
export async function GET(req: Request) {
// Verify cron secret
if (req.headers.get('Authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Unauthorized', { status: 401 })
}
const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)
await db.conversation.deleteMany({ where: { createdAt: { lt: cutoff } } })
return Response.json({ ok: true })
}