GDPR Art. 20 gives data subjects the right to receive their personal data in a structured, commonly used, machine-readable format and to transmit it to another controller. CCPA §1798.110 and LGPD Art. 18 impose equivalent portability rights. Without a working data export endpoint, you cannot fulfill these rights — which means any user who files a data portability request triggers a regulatory obligation you cannot meet programmatically. The practical consequence is manual database queries under time pressure, which introduces error risk and compliance delay. This is also a competitive signal: users who can export their data are less anxious about vendor lock-in.
Medium because failure to provide portability on request triggers a mandatory regulatory response timeline (one month under GDPR Art. 12) and constitutes a standalone violation of Art. 20.
Create an authenticated export endpoint at app/api/user/export/route.ts that gathers all user data and returns it as a downloadable JSON file.
// app/api/user/export/route.ts
import { getServerSession } from 'next-auth'
import { db } from '@/lib/db'
export async function GET() {
const session = await getServerSession()
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 })
const userId = session.user.id
const [profile, activity, purchases] = await Promise.all([
db.user.findUnique({ where: { id: userId },
select: { id: true, email: true, name: true, createdAt: true } }),
db.activityLog.findMany({ where: { userId }, orderBy: { createdAt: 'desc' } }),
db.order.findMany({ where: { userId }, include: { items: true } }),
])
return new Response(JSON.stringify({ exportedAt: new Date().toISOString(), profile, activity, purchases }, null, 2), {
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="my-data-${userId}.json"`,
},
})
}
Add a "Download my data" button to the user settings page. Rate-limit the endpoint to prevent abuse (e.g., one export per 24 hours per user).
ID: data-protection.user-rights-access.data-export
Severity: medium
What to look for: Enumerate every relevant item. Look for a user settings or profile page with a "Download My Data," "Export Data," or "Data Portability" option. Check API routes for a data export endpoint (common paths: /api/user/export, /api/account/download, /api/me/data). Inspect what the export includes: does it cover all data types (profile information, activity history, uploaded files, messages, purchase history)? Is the format machine-readable (JSON or CSV)? Does the export require authentication to access? Is there a rate limit on export requests to prevent abuse?
Pass criteria: At least 1 of the following conditions is met. Authenticated users can trigger a download of all their personal data from a settings or profile page. The export is in a standard, machine-readable format (JSON or CSV). The export covers all major data categories held about the user. The endpoint requires authentication and has reasonable rate limiting.
Fail criteria: No data export feature exists. Export exists but is incomplete (e.g., only profile info, missing activity history or purchases). Export is in a proprietary or non-machine-readable format.
Skip (N/A) when: The application collects no personal data beyond what is necessary to authenticate (e.g., email and hashed password with no other data stored per user).
Detail on fail: Example: "No data export feature found. No /api/user/export route or equivalent exists. Users cannot exercise their right to data portability." or "Export endpoint exists but only returns profile fields; purchase history and activity logs are excluded.".
Remediation: Create a data export API endpoint and wire it to a settings page:
// app/api/user/export/route.ts
import { getServerSession } from 'next-auth'
import { db } from '@/lib/db'
export async function GET() {
const session = await getServerSession()
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 })
const userId = session.user.id
// Gather all user data
const [profile, activity, purchases, preferences] = await Promise.all([
db.user.findUnique({ where: { id: userId },
select: { id: true, email: true, name: true, createdAt: true } }),
db.activityLog.findMany({ where: { userId }, orderBy: { createdAt: 'desc' } }),
db.order.findMany({ where: { userId }, include: { items: true } }),
db.userPreferences.findUnique({ where: { userId } }),
])
const exportData = {
exportedAt: new Date().toISOString(),
formatVersion: '1.0',
profile,
activityHistory: activity,
purchases,
preferences,
}
return new Response(JSON.stringify(exportData, null, 2), {
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="my-data-${userId}.json"`,
},
})
}
Add a "Download my data" button to the user settings page that links to this endpoint.