Overview
The "Download My Data" feature gives users the ability to export a complete copy of their personal data in a portable format (JSON + CSV). This is a GDPR Article 20 requirement ("Right to Data Portability") and a trust-building feature for privacy-conscious users.
Data Exported (User-Level)
| Section | Contents |
|---|---|
| Profile | Name, email, phone, avatar URL, creation date |
| Connected Accounts | Provider names, connection dates |
| Workspace Memberships | Workspace name, role, join date (for each workspace) |
| Notification Preferences | Current toggle states |
| Security Events (future) | Login history, 2FA events |
Not included: Workspace-specific content (e.g., blog posts, ecommerce data) — that is covered by a separate Workspace Data Export feature (future).
Export Format
A ZIP file containing:
data-export-<userId>-<timestamp>.zip
├── profile.json — Personal profile fields
├── memberships.json — Workspace memberships + roles
├── notification_prefs.json — Notification toggle states
├── connected_accounts.json — Auth providers
└── README.txt — Explains the format and what's includedBackend API
POST /manager/profile/export
- Auth: Must be authenticated (own data only)
- Rate limit: Max 1 export request per 24 hours per user
- Process:
- Query all user data across tables
- Bundle into a ZIP (using a library like
jsziporarchiveron a Cloudflare Worker withfflate) - Upload ZIP to R2 with a 1-hour pre-signed download URL
- Return the pre-signed URL
GET /manager/profile/export/status
- Check if a pending export is in progress or has a recent download available
Frontend
Location: /settings/profile or /settings/account
A simple section with:
- "Request Data Export" button
- On click: shows spinner → "Your export is being prepared"
- On completion (polling or toast): "Your data export is ready — [Download ZIP]"
- Download link expires after 1 hour (shown in UI)
- Cooldown message: "You can request a new export in X hours"
Schema Addition
No new tables required. Add a lightweight user_export_requests table to track cooldowns and export status:
export const userExportRequests = pgTable("user_export_requests", {
id: text("id").primaryKey(),
userId: text("user_id").references(() => users.id).notNull(),
status: text("status").notNull(), // pending | ready | expired
downloadUrl: text("download_url"), // R2 pre-signed URL (set when ready)
expiresAt: timestamp("expires_at"), // 1 hour after generation
requestedAt: timestamp("requested_at").defaultNow().notNull(),
})Implementation Order
- Add
userExportRequeststo schema + migrate - Build the data aggregation query (joins
users,userAuthProviders,memberships,userNotificationPreferences) - Implement ZIP bundling in a Cloudflare Worker (use
fflatefor WASM-compatible compression) - Upload ZIP to R2 and generate 1-hour presigned download URL
- Add
POST /profile/exportandGET /profile/export/statusendpoints - Build the frontend section (ideally in
/settings/accountunder a "Data & Privacy" card)
Privacy & Compliance Notes
- The export ZIP must be generated server-side — never expose raw DB queries to the client
- The pre-signed download URL should expire in 1 hour
- After expiry, the ZIP should be automatically deleted from R2 (use R2 object lifecycle rules)
- Inform the user clearly in
README.txtwhat data is included and what is not - Log the export request in the audit log (when implemented):
user.data_export_requested