EMZETT.
Login

GDPR Data Export (Art. 15/20)

In short: The right to be given, in a structured form, all personal data a company has stored about you — the right of access (Art. 15 GDPR) and the right to data portability (Art. 20 GDPR).

In more detail: In practice usually implemented as an “export data” button in your own account, which gathers all records linked to the logged-in user on the server and offers them for download, e.g. as a JSON file. What matters is a conscious decision about WHAT belongs in the export: purely technical/security-relevant fields (password hash, push subscription) and personal data of THIRD parties (e.g. who else was involved in a shared support ticket) don’t belong in it.

Our context: At Emzett, app/utils/gdprExport.ts (exportUserData) collects account data, orders, points history, support tickets, chat messages etc. for the logged-in user — deliberately WITHOUT passwordHash and pushSubscription. Runs through the general rate-limit check, since a complete data export is a comparatively expensive operation.

In Depth

Art. 15 GDPR (right of access) and Art. 20 GDPR (right to data portability) are often implemented together as a “data export” in practice, but legally they differ in scope: Art. 15 obliges you to provide information about all personal data processed, including processing purposes, recipients and storage period — regardless of how the data was collected. Art. 20 is narrower: it only applies to data that the data subject provided to the company themselves (e.g. profile data, uploaded content) and whose processing is based on consent or a contract — but not to data the company derived or calculated itself (e.g. an internally calculated customer value score).

A common legal requirement is the response deadline: an access request must be answered “without undue delay and in any event within one month” (Art. 12(3) GDPR), with the possibility of extending this by two further months for complex requests. An automated self-service export in the account area undercuts this deadline from the outset, because users can fetch the data themselves immediately instead of having to submit a manual request and wait for it to be processed — which at the same time considerably reduces the support effort on the company’s side.

Technically, the handling of linked data that also concerns other people is particularly tricky. A chat history or a shared support ticket typically contains messages from several participants — a naive export of “all records containing my user ID” would unintentionally also disclose personal data of third parties, which can itself constitute a GDPR violation. Common approaches are either to export only your own messages within a thread, to anonymise other people’s contributions (e.g. “Other user” instead of the real name), or to include complete threads only if the requesting person is the sole or principally responsible participant.

// Simplified pattern for a GDPR data export
async function exportUserData(userId: string) {
  const [account, orders, tickets] = await Promise.all([
    db.query.users.findFirst({ where: eq(users.id, userId) }),
    db.query.orders.findMany({ where: eq(orders.userId, userId) }),
    db.query.tickets.findMany({ where: eq(tickets.userId, userId) }),
  ]);
 
  // Deliberately exclude security-relevant fields
  const { passwordHash, pushSubscription, ...safeAccount } = account;
 
  return { account: safeAccount, orders, tickets };
}

See also: Small business regulation (§19 UStG), Right of withdrawal for digital content (§ 356(5) BGB), Account deletion with an objection period (soft delete + anonymisation), GDPR, Audit log