CCPA §1798.100 and §1798.110 grant California residents the right to know what personal information a business has collected about them and to receive it in a portable format. GDPR Article 15 (right of access) and Article 20 (right to data portability) impose equivalent obligations for EU residents. Without a data export path, every access request requires a manual database query — slow, expensive, and inconsistent across requests. An export that only returns the contact profile while omitting consent history, engagement events, and form submissions is an incomplete response to a legal right, leaving the business exposed to regulatory action.
Low because the operational gap (no export tooling) only becomes a compliance violation when a data subject actually exercises their access or portability right — but that trigger is entirely outside your control.
Implement a GET /api/admin/contacts/:id/export endpoint backed by a service that aggregates all PII categories in one call:
// src/lib/compliance/export.ts
export async function exportContactData(contactId: string): Promise<ContactDataExport> {
const [contact, consentHistory, emailEvents, formSubmissions] = await Promise.all([
db.contact.findUnique({ where: { id: contactId } }),
db.consentRecord.findMany({ where: { contactId }, orderBy: { createdAt: 'asc' } }),
db.emailEvent.findMany({ where: { contactId }, orderBy: { occurredAt: 'asc' } }),
db.formSubmission.findMany({ where: { contactId }, orderBy: { submittedAt: 'asc' } }),
])
return {
exportedAt: new Date().toISOString(),
contactId,
profile: contact,
consentHistory,
emailEngagement: emailEvents,
formSubmissions,
}
}
Verify the export is scoped to one contact — run a query in staging that confirms no other contact's records appear in the response. Protect the endpoint with admin-only authentication.
ID: compliance-consent-engine.data-subject-rights.ccpa-export
Severity: low
What to look for: CCPA grants California residents the right to know what personal data is collected about them (access right) and to receive it in a portable format. Look for a data export path — an endpoint or admin action that collects all personal data associated with a contact and returns it as JSON, CSV, or another machine-readable format. Check that it covers all data categories (contact info, consent history, email event history, form submissions).
Pass criteria: A data export path exists that produces a structured, machine-readable output containing all personal data categories held about a contact. The export is scoped to a single contact and does not leak other contacts' data. Count all data categories included in the export — at least 3 must be present (profile, consent history, engagement events).
Fail criteria: No data export capability exists. Or export exists but only returns partial data (e.g., contact record only, missing event history and consent log).
Skip (N/A) when: Application provably serves no California residents and is not subject to CCPA.
Detail on fail: "No data export endpoint or admin action found — access right requests would require manual database queries" or "exportContactData() returns contacts and consent_records but omits email_events and form_submissions"
Remediation: Implement a comprehensive export:
export async function exportContactData(contactId: string): Promise<ContactDataExport> {
const [contact, consentHistory, emailEvents, formSubmissions] = await Promise.all([
db.contact.findUnique({ where: { id: contactId } }),
db.consentRecord.findMany({ where: { contactId }, orderBy: { createdAt: 'asc' } }),
db.emailEvent.findMany({ where: { contactId }, orderBy: { occurredAt: 'asc' } }),
db.formSubmission.findMany({ where: { contactId }, orderBy: { submittedAt: 'asc' } }),
])
return {
exportedAt: new Date().toISOString(),
contactId,
profile: contact,
consentHistory,
emailEngagement: emailEvents,
formSubmissions,
}
}