GDPR Art. 20 gives every EU user a statutory right to receive a structured, machine-readable copy of the personal data you hold about them, and CCPA §1798.110 gives California users a parallel right-to-know. Both have a one-month (GDPR Art. 12) response clock that starts the moment a request lands in a support inbox. Without a working export endpoint the only way to satisfy a portability request is a panic-mode manual database dump — which regulators treat as a risk factor in its own right, because ad-hoc dumps leak data they should not include. The GDPR fine ceiling for Art. 20 non-compliance is 4% of global turnover or €20M; CCPA AG enforcement has reached $7,500 per affected consumer. AI coding tools reliably build login and profile-edit flows, but rarely scaffold a /api/me/export route, because training corpora skew heavily toward CRUD-happy-path code and away from regulator-facing plumbing. The quiet failure mode is a live site that looks compliant until the first SAR lands and nobody knows where to point it.
High because missing export directly blocks Art. 20 / §1798.110 compliance on a one-month regulator clock, scales liability per-user, and cannot be handwaved as "best effort" — the obligation is binary.
Add an authenticated GET route at app/api/me/export/route.ts that fetches every row tied to the requesting user across your main tables, serializes to JSON, and returns it with a Content-Disposition: attachment; filename="my-data.json" header.
// app/api/me/export/route.ts
export async function GET() {
const session = await getServerSession()
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 })
const data = await db.user.findUnique({
where: { id: session.user.id },
include: { orders: true, activityLog: true, preferences: true },
})
return new Response(JSON.stringify(data, null, 2), {
headers: { 'Content-Type': 'application/json', 'Content-Disposition': 'attachment; filename="my-data.json"' },
})
}
Rate-limit to one export per user per 24 hours. Wire a "Download my data" button in app/settings/page.tsx. Deeper coverage of the portability workflow (file uploads, third-party processor data, completeness checks) lives in the gdpr-readiness Pro audit.
project-snapshot.legal.data-export-endpoint-existshighpackage.json, auth routes, User model). If none, skip. Otherwise enumerate export surfaces. Look for a route at one of app/api/user/export, app/api/me/export, app/api/account/export, app/api/privacy/data, app/api/export, app/me/download, app/settings/export, pages/api/user/export, or a server action whose name matches /^(export|download|request)(UserData|Account|MyData|DataExport)$/i. Inspect the handler body: it must actually build a payload from user data and return it (JSON, CSV, ZIP), typically via db.user.findUnique / supabase.from('users').select followed by new Response(JSON.stringify(...)) with a Content-Disposition: attachment header or equivalent. Reject stubs: a route returning { message: 'Coming soon' }, a page that only links to mailto:privacy@ with no server handler, or a response body that is literally empty."No auth library, auth routes, or user model detected — project has no user accounts."{ message: 'Contact support' }, redirecting to an email link, throwing 501 Not Implemented, or commented-out TODO. The endpoint must actually emit data."<file>: <line referencing auth>"."Export handler at <path>; format: <json|csv|zip>; includes: <tables queried>.""User accounts present (Clerk in package.json, sign-in route at app/sign-in/) but no export route found under app/api/user/, app/api/me/, app/api/account/, or app/api/export/." or "Route at app/api/me/export/route.ts exists but handler body only returns new Response('Coming soon', { status: 501 })."app/api/me/export/route.ts example above in remediation_prose. Rate-limit to one export per user per 24 hours and surface the trigger in app/settings/page.tsx. Deeper coverage lives in the gdpr-readiness Pro audit.