# Authenticated users can download all personal data in portable format

- **Pattern:** `ab-000843` (`data-protection.user-rights-access.data-export`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000843
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000843)

## Why it matters

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.

## Severity rationale

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.

## Remediation

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.

```ts
// 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).

## Detection

- **ID:** `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:

  ```ts
  // 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.

---

## External references

- gdpr Art. 20
- gdpr Art. 15
- ccpa §1798.110
- lgpd Art. 18
- gdpr Art. 12

Taxons: privacy-consent

HTML version: https://auditbuffet.com/patterns/ab-000843
