COPPA §312.10 and §312.5 require operators to retain children's personal information 'only as long as reasonably necessary to fulfill the purpose for which it was collected,' then to delete it securely. Indefinite retention of child data is independently actionable — the FTC has cited absence of a documented retention schedule in enforcement actions even when consent and age-gate practices were otherwise adequate. GDPR Article 5(1)(e) imposes a parallel 'storage limitation' principle. When parental consent is revoked, all data collected under that consent must be deleted — an orphaned analytics event or usage log tied to a deleted child account is a retention violation.
Info because retention violations are prospective rather than immediate data exposure, but the FTC treats indefinite child data retention as a §312.10 violation independent of how the data was originally collected.
Implement a cron route that deletes inactive child accounts and all associated records on a defined schedule, and ensure consent revocation triggers a cascade delete across every table that holds child data.
// app/api/cron/child-data-retention/route.ts
export async function GET() {
const cutoff = new Date()
cutoff.setMonth(cutoff.getMonth() - 12) // 12-month inactivity threshold
const inactive = await db.user.findMany({
where: { accountType: 'child', lastActiveAt: { lt: cutoff } },
select: { id: true }
})
for (const child of inactive) {
await db.$transaction([
db.analyticsEvent.deleteMany({ where: { userId: child.id } }),
db.usageLog.deleteMany({ where: { userId: child.id } }),
db.parentalConsent.deleteMany({ where: { childUserId: child.id } }),
db.user.delete({ where: { id: child.id } }),
])
}
return Response.json({ purged: inactive.length })
}
In your privacy policy state the retention period explicitly: 'Inactive child accounts are deleted after [N] months. When parental consent is revoked, the child's account and all associated data are permanently deleted within [N] business days.'
ID: coppa-compliance.operator-obligations.retention-limited
Severity: info
What to look for: Count all relevant instances and enumerate each. COPPA requires operators to retain personal information collected from children "only as long as reasonably necessary to fulfill the purpose for which the information was collected." Look for whether the application has a shorter retention period for child account data compared to adult account data. Check for: automated deletion of inactive child accounts after a defined period, deletion of child data when parental consent is revoked, a clear statement in the privacy policy about how long child data is retained. Look for cron jobs or scheduled functions that clean up child data. Also check whether child data in analytics systems or logs is purged on the same schedule.
Pass criteria: Child data has a defined retention period documented in the privacy policy. When a child account is deleted (by parent revocation or account inactivity), all associated personal data is permanently deleted. There is an automated mechanism to enforce retention limits on inactive child accounts.
Fail criteria: No specific retention period for child data is documented. Child data is retained indefinitely with no automated cleanup. When parental consent is revoked, only the account is deleted but not associated data in analytics, logs, or other systems.
Skip (N/A) when: The application hard-blocks all users under 13 and no parental consent workflow exists.
Detail on fail: Example: "No specific retention schedule for child data found. Privacy policy does not mention how long children's data is retained. No automated cleanup for inactive child accounts." or "Consent revocation deletes the child user record but analytics events and usage logs linked to the child's session remain indefinitely.".
Remediation: Implement a dedicated retention schedule and deletion cascade for child accounts:
// app/api/cron/child-data-retention/route.ts
export async function GET() {
// Delete inactive child accounts after 12 months of inactivity
const cutoff = new Date()
cutoff.setMonth(cutoff.getMonth() - 12)
const inactiveChildren = await db.user.findMany({
where: {
accountType: 'child',
lastActiveAt: { lt: cutoff },
},
select: { id: true }
})
for (const child of inactiveChildren) {
await deleteChildAccountData(child.id)
}
return Response.json({ purged: inactiveChildren.length })
}
async function deleteChildAccountData(childId: string) {
// Delete in dependency order, then delete the user record
await db.$transaction([
db.analyticsEvent.deleteMany({ where: { userId: childId } }),
db.usageLog.deleteMany({ where: { userId: childId } }),
db.parentalConsent.deleteMany({ where: { childUserId: childId } }),
db.user.delete({ where: { id: childId } }),
])
}
In your privacy policy, state: "We retain children's personal information only for as long as the account is active or as needed to provide the service. Inactive child accounts are deleted after [N] months. When parental consent is revoked, the child's account and all associated data are permanently deleted within [N] business days."