COPPA §312.6 grants parents the right to review the information collected from their child and to revoke consent at any time. Without a durable consent record, an operator cannot verify that consent was obtained, cannot respond to a parental review request with evidence, and cannot trace whether a specific child account was legitimately authorized. If the FTC investigates and the operator cannot produce records showing when consent was given, by whom, and via which method, the absence of records is treated as an absence of consent. The consent record is also the anchor for the parent's revocation rights — without it, revocation cannot be cleanly executed.
Medium because the absence of consent records does not itself expose child data, but it strips the operator's legal defense and makes parental review and revocation mechanically impossible to fulfill.
Create a dedicated parental_consents table that stores the child account link, parent identifier, consent method, and timestamp. Write the record before activating the child account.
-- Migration: create parental_consents table
CREATE TABLE parental_consents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
child_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
parent_email TEXT NOT NULL,
method TEXT NOT NULL, -- 'email_link', 'signed_form', etc.
consented_at TIMESTAMPTZ NOT NULL DEFAULT now(),
revoked_at TIMESTAMPTZ,
metadata JSONB DEFAULT '{}'
);
// Record consent before creating the child user record
await db.parentalConsent.create({
data: {
childUserId: child.id,
parentEmail: pending.parentEmail,
method: 'email_link',
consentedAt: new Date(),
metadata: { ip: req.headers.get('x-forwarded-for'), tokenUsed: params.token }
}
})
ID: coppa-compliance.parental-consent.consent-records
Severity: medium
What to look for: Count all relevant instances and enumerate each. Look at the database schema or data store for consent records. When a parent provides consent, is a record created that captures: the timestamp of consent, the method used (email-link, signed form, credit card, etc.), and enough information to identify the parent (their email address or an identifier)? Check whether the consent record is stored separately from the child's account record (so it persists even if the child account is modified). Look for whether the consent record links back to the child account it authorizes. Check the retention policy for consent records — COPPA requires records to be kept for a reasonable period.
Pass criteria: A parental_consent (or equivalent) table or record exists per child account, storing at minimum: parent identifier (email), consent method, timestamp of consent given, and a link to the child account ID. Records are retained for the lifetime of the child's account.
Fail criteria: No consent records are stored — consent is collected but not logged. Consent records lack the timestamp, method, or parent identity. Consent records are stored in a session that is cleared after use, leaving no durable record.
Skip (N/A) when: The application hard-blocks all users under 13 and no parental consent workflow exists.
Detail on fail: Example: "Parental consent email is sent and confirmed, but no consent record is created in the database. If a parent later requests proof of consent, there is no audit trail." or "Consent records exist but only store the parent email and timestamp — the consent method and which child account was authorized are not captured.".
Remediation: Create a durable consent audit table:
-- Migration: create parental_consents table
CREATE TABLE parental_consents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
child_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
parent_email TEXT NOT NULL,
method TEXT NOT NULL, -- 'email_link', 'signed_form', 'credit_card', etc.
consented_at TIMESTAMPTZ NOT NULL DEFAULT now(),
revoked_at TIMESTAMPTZ,
metadata JSONB DEFAULT '{}' -- method-specific details (IP, user-agent, etc.)
);
// Record consent before activating the child account
await db.parentalConsent.create({
data: {
childUserId: childAccount.id,
parentEmail: pending.parentEmail,
method: 'email_link',
consentedAt: new Date(),
metadata: {
confirmationIp: req.headers.get('x-forwarded-for'),
confirmationUserAgent: req.headers.get('user-agent'),
tokenUsed: params.token,
}
}
})