COPPA §312.5 and §312.4 prohibit collecting any personal information from a child under 13 until verifiable parental consent is obtained. 'Verifiable' is the operative word: a checkbox on the child's own signup form is not consent from the parent — it is consent from the child, which COPPA explicitly does not recognize. Creating a user record and storing the child's email and date of birth before the parent has confirmed is a COPPA §312.5 violation from the moment the INSERT executes. The FTC has levied eight-figure fines (FTC v. Google/YouTube, 2019: $170M) specifically for collecting children's data before consent.
Critical because any personal data stored before parental consent is confirmed constitutes an active COPPA §312.5 violation — the legal exposure exists for every child record created without prior consent.
Implement a two-stage flow: Stage 1 stores only a short-lived pending-consent record (not a user account) and emails the parent. Stage 2, triggered by the parent's confirmation link, creates the actual user account.
// Stage 1: app/api/auth/child-signup/route.ts
// Store NOTHING in the users table yet
export async function POST(req: Request) {
const { childEmail, dateOfBirth, parentEmail } = await req.json()
if (computeAge(new Date(dateOfBirth)) >= 13) {
return Response.json({ error: 'Use the standard signup flow.' }, { status: 400 })
}
const token = crypto.randomUUID()
await db.pendingConsentRequest.create({
data: { token, childEmail, parentEmail, dateOfBirth,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) }
})
await sendParentConsentEmail({ parentEmail, childEmail, token })
return Response.json({ status: 'awaiting_parental_consent' })
}
The child's account and any personal data are created only after the parent's confirmation link is clicked and a parental_consents record is written.
ID: coppa-compliance.parental-consent.verifiable-consent
Severity: critical
What to look for: Count all relevant instances and enumerate each. If the application allows child accounts (users under 13), look for the parental consent workflow. COPPA requires "verifiable" parental consent — meaning a method that gives reasonable assurance that the person providing consent is actually the parent or legal guardian, not another child. Search for: parent email collection before child account activation, a consent verification link sent to the parent email, a pending account state that prevents the child from accessing the service until the parent confirms, and a server-side flag (parentalConsentStatus, consentVerified, or similar) that gates account access. Critically, check whether the child account is created and data is stored before parental consent is received — this is a COPPA violation. No personal information (name, email, birth date, usage data) should be stored on the child's behalf until and unless the parent has consented.
Pass criteria: If child accounts are supported: no personal information is collected from or stored about a child until verifiable parental consent is received. The consent workflow collects the parent's contact information, sends a direct notice to the parent, and creates or activates the child's account only after the parent confirms. The child account is in a pending, non-functional state (or no account exists at all) until consent is verified.
Fail criteria: Child accounts are created and personal data (email, name, usage events) is stored before parental consent is obtained. The "parental consent" step is a checkbox on the child's signup form rather than an out-of-band verification to the parent. No parental consent mechanism exists despite the application allowing users under 13.
Skip (N/A) when: The application hard-blocks all users under 13 at registration and no child accounts are possible (verified by the underage-blocking check passing).
Detail on fail: Specify the gap. Example: "Application creates a user record and stores the child's email and birth date immediately upon signup, before any parental consent step. Parent email is collected afterward but consent is never verified before data storage." or "No parental consent workflow found. Child accounts are created identically to adult accounts with no parental involvement.".
Remediation: Implement a two-stage child account creation flow where no data is committed until after parental consent:
// Stage 1: Child initiates signup — store nothing yet
// app/api/auth/child-signup/route.ts
export async function POST(req: Request) {
const { childEmail, dateOfBirth, parentEmail } = await req.json()
const age = computeAge(new Date(dateOfBirth))
if (age >= 13) {
return Response.json({ error: 'Use the standard signup flow.' }, { status: 400 })
}
// Store a short-lived pending consent request — NOT a user account
// This record holds only what's needed to resume after parent consent
const token = crypto.randomUUID()
await db.pendingConsentRequest.create({
data: {
token,
childEmail, // stored only temporarily — deleted if consent not given
parentEmail,
dateOfBirth,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
}
})
// Send direct notice to parent (see direct-notice-parents check)
await sendParentConsentEmail({ parentEmail, childEmail, token })
// Tell the child: "We've sent an email to your parent. Check with them to complete signup."
return Response.json({ status: 'awaiting_parental_consent' })
}
// Stage 2: Parent clicks consent link — NOW create the child's account
// app/api/auth/parent-consent/[token]/route.ts
export async function GET(req: Request, { params }: { params: { token: string } }) {
const pending = await db.pendingConsentRequest.findUnique({
where: { token: params.token, expiresAt: { gt: new Date() } }
})
if (!pending) {
return Response.json({ error: 'Consent link expired or invalid.' }, { status: 404 })
}
// Record consent before creating user
const consentRecord = await db.parentalConsent.create({
data: {
parentEmail: pending.parentEmail,
childEmail: pending.childEmail,
method: 'email_link',
consentGivenAt: new Date(),
}
})
// Now create the child account
await db.user.create({
data: {
email: pending.childEmail,
dateOfBirth: pending.dateOfBirth,
parentalConsentId: consentRecord.id,
accountType: 'child',
}
})
// Clean up the pending request
await db.pendingConsentRequest.delete({ where: { token: params.token } })
return Response.redirect('/parent-consent-confirmed')
}