How Do I Let a User Download All the Data I Have on Them?
6 min read
If a user asks for a copy of everything you hold on them, you have to be able to hand it over. Under GDPR and CCPA that is a legal right, not a favour. The good news: for a small beta app you do not need a fancy feature. You need a button (or even a manual process) that gathers every row tied to that user and returns it as a file. That is it.
Why this happens
You asked your AI agent to build sign-up, profiles, and whatever your app does.
It built exactly that. Nobody asked it to build a "download my data" feature,
so it did not, and it never mentioned that the law expects one. The obligation
is invisible in the demo because no reviewer, regulator, or annoyed user has
shown up yet. The data itself is already spread across several tables: a
users row, some profiles, maybe orders, messages, uploads, and rows
in tables you have half-forgotten. Export is simply the act of collecting all of
those pieces for one person in one place.
How to check
- Ask yourself where a single user's data actually lives. Open your
database and list every table with a
user_id(oremail) column. Each one is part of that person's record. - Try to produce your own export by hand. Pick a real user id and see if you can gather every row they own into one file. If that takes you an hour of fishing around, an automated version will save you every time.
- Check whether you already promised one. If your privacy policy or terms mention a right to access or data portability, you are on the hook to deliver it, feature or no feature.
The fix
- Map the tables first. Write down every table keyed to a user. Include auth records, profile data, content they created, and activity logs. If you are not sure what you store, see what data your app is actually storing.
- Write one server-side function that gathers it. On Supabase, a Postgres
function or an edge function that runs
select * where user_id = $1across each table works. On Firebase, a Cloud Function that reads each collection byuid. Keep it on the server so it runs with trusted access, never in the browser. - Return it as JSON or a zip. JSON is fine for a beta. Include a top-level key per table so the user can see what each part is. If they uploaded files, include download links or bundle the files in a zip.
- Gate it hard. Only the signed-in user may export their own data, and an
admin may export on request. Confirm the request is authenticated and the
user_idmatches the caller. An export endpoint that trusts auser_idfrom the query string is a data leak for every account. - Manual is allowed. For a small beta, a documented process where you run the function yourself when someone emails you is a legitimate answer. You must respond within about a month under GDPR. You do not need a self-serve button on day one.
The trap to avoid
Do not just dump your entire database and mail it over. An export must contain only that one user's data, never anyone else's rows mixed in. The other common mistake is building a shiny self-serve export button that skips authentication, so anyone can pull any account by changing a number in the URL. That turns a compliance feature into the breach it was meant to prevent. Scope to the caller, prove the caller is who they say, and return only their rows.
Where this fits
Data export is one of a small cluster of user-rights features, alongside account and data deletion, that real users and app stores start asking about the moment you launch. It is easy to defer and easy to get subtly wrong, especially the part where an export accidentally leaks someone else's data. The free Readiness Report checks whether your endpoints are properly scoped to their caller and flags the user-rights gaps worth closing before beta. If you would rather have the export function and its access checks built and handed to you in order, that is what the Finishing Pass is for.