You must let a customer download everything you hold about them in a machine-readable form, as Article 20 of the GDPR requires. The product holds profile data, 4 years of activity events, uploaded files, support conversations and model-derived scores. Design the export.
Show the full answer Hide the answer
Requirements that drive the structure
Three constraints shape it before any format is chosen. The scope is narrower than "everything you hold": portability, under a right that has applied in the EU since 2018, covers personal data the subject provided and data observed about them, in a structured, commonly used, machine-readable format. Inferred and derived data — a risk score, a recommendation ranking — is generally outside portability even where it is within the right of access, and conflating the two is both over-disclosure and a competitive problem.
Second, the volume is unbounded. Four years of events and uploaded files is gigabytes for some accounts and kilobytes for most, so the export is a long-running operation rather than a request.
Third, and most often missed: an export is the single most valuable object an account takeover can produce. It concentrates everything into one downloadable file, and the attack sequence that follows is request the export, then change the password. Choose the step-up authentication and the notify-and-delay controls before the file format.
The design
- Classify the data model first, field by field, into provided, observed, derived and third-party-related. This is the work; the file format is trivial once it is done. The classification also becomes the artefact you show a regulator.
- Run it as an operation resource: a request creates a job, the job is idempotent on a key so a retried request does not generate a second export, and a signed URL with a short lifetime delivers the result. Publish the lifetime as part of the product.
- Re-authenticate at request time and again at download, and deliver the link through a channel separate from the session that requested it. A step-up authentication requirement on export is the control that matters most, and it costs one screen.
- Delay and notify. A deliberate hold of some hours, with a notification to the account's verified contact and a cancellation link, turns a silent exfiltration into something the real owner can stop. Document the delay so it does not read as obstruction.
- Exclude other people's data. Support conversations and shared content contain third-party personal data; a group chat transcript is not solely the requester's. Redact or summarise, and record the rule you applied.
- One stable schema with a version field, documented, so an export taken today is readable next year and a consuming service can parse it. JSON plus the original files in a folder structure, with a manifest, is sufficient and sensible.
- Rate-limit and log every export as a security-relevant event, because the pattern "export then password change" is a takeover signature.
What it costs
A classification exercise across the whole data model, which nobody has budgeted and which touches every team; an async job system; a storage lifecycle for artefacts; and a permanent obligation to keep the export in step with the schema. Budget a quarter for the first version in a mature product, most of it classification rather than code — for a 300-field data model, expect 2 to 3 days per owning team just to classify. The ongoing cost is a test that fails when a new personal-data field is added without a classification, which is the only way the export stays complete.
How it fails
- The export that is three months behind the schema, because a new field was added and nothing failed.
- The forever-valid link sitting in a chat history, containing everything about a person.
- Over-disclosure of derived data, handing a competitor your scoring model's outputs, or of another person's data in a conversation.
- An export that times out for the largest accounts, so the obligation is met for most users and not for the ones most likely to complain.
When this is over-engineering
For a product holding a profile and a settings object, the export is a synchronous JSON endpoint and the whole apparatus above is waste. The async machinery becomes necessary when any account's export cannot be produced inside a request. And where the dataset is genuinely small, the delay-and-notify control still earns its place: it is cheap, and account takeover does not care how small your data model is.