Data Export — Request and Download Your Data
Users can request a complete export of their Objectuve data at any time, fulfilling the public benefit corporation obligation to user data ownership. Exports are processed asynchronously and delivered via email with a 30-day download window.
Requesting an Export
Data export is accessible from Settings > Privacy > Your data. The feature is presented as a card with a clear, action-forward button: "Export my data". Tapping triggers a mutation that:
- Validates the request — Each email address is limited to one export request per 24 hours. If a user requests while still within the cooldown window, they see: "You can request another export in X hours."
- Creates a GDPR request record — Backend creates a
GdprRequestwith statusreceivedand metadata{ source: 'in_app_self_service' }. - Enqueues the export job —
Gdpr::BuildExportBundleJobis dispatched to process asynchronously.
Users see five possible states on the card:
| State | Trigger | UI | Duration |
|---|---|---|---|
| Idle | Initial state or after cooldown expires | "Export my data" button enabled | N/A |
| Cooldown | User has made a request within last 24h | Button disabled; shows "You can request another export in X hours" | 24 hours |
| Requesting | Mutation accepted; job queued | "Preparing your export..." spinner + disabled button | ~1–5 seconds |
| Processing | Job running; user notified | "Your export is on its way. We'll email you when it's ready." | ~5–60 seconds (varies by data volume) |
| Ready | Job complete; email sent | "Your export is ready. Download expires [date]." + Download button | 30 days |
What's Included
The export is a single ZIP archive containing two representations of the same data:
data.json— Machine-readable full dump of all tables (JSON array format).- Per-table CSVs — Human-friendly spreadsheet view of each table.
Exported Tables
| Table | Description | Notes |
|---|---|---|
user | User profile | Includes username, email, first/last name, account creation/update dates, verification status |
goals | All goals (active + archived) | data.json carries every column; the goals.csv file uses a curated, stable header set instead of a full column dump — external_id, name, description, life_area, recurrence_type, recurrence_days, recurrence_interval, duration_minutes, target_amount, unit, target_date. This is the same contract Data Import reads, so streak fields and visibility are deliberately absent from the CSV — see that page for why. |
habit_completions | Habit check-in history | data.json carries every column; habit_completions.csv uses the same curated set the importer reads: goal_external_id, completed_date, note. |
goal_events | Goal updates and milestones | Progress logs, event descriptions, timestamps, and media references |
mood_logs | Daily mood entries | Mood score, date, and timestamp |
user_actions | Gamification events | Badge unlocks, level-ups, streak milestones, and XP gains |
milestones | Goal sub-goals | Milestone title, target date, and completion status |
user_devices | Registered devices | Device tokens, platform (iOS/Android/web), and registration date |
community_memberships | Community participation | Community ID, role, and join date |
ai_employee_memories | AI coaching history | Memories recorded about the user by Coach, for personalization |
admin_actions | Administrative logs | Actions taken by admins (e.g., moderation, user support) |
feedback_posts | User feedback submissions | Community feedback board posts authored by the user |
feedback_comments | Feedback discussion | Comments the user authored on feedback posts |
What's NOT included:
- Third-party community posts or comments authored by other users (only user-owned content)
- Other users' admin PII or sensitive metadata
- Deleted data (soft-deleted records are hard-deleted from the export for privacy)
share_events— deliberately excluded even though it's user-owned (user_id). It's a private "I shared this" receipt (kind, subject, channel, timestamp), not content the user authored or would recognize as "their data," and the celebration card's own content is derived from data already exported viagoals/user_actions/etc.Gdpr::ExportBuilder(rails_api/app/services/gdpr/export_builder.rb) has noshare_eventssectionshare_attributions— same rationale and same silent exclusion asshare_eventsabove: it's a private referral-attribution record (whichShareEventa signup is attributed to, and when), not authored content.Gdpr::ExportBuilderhas noshare_attributionssection either
Download & Expiry
The export is uploaded to a private Google Cloud Storage bucket. The download link:
- Delivery: Sent via email signed by Mailtrap template
- Format: 30-day signed URL with JWT authentication (
Gdpr::ExportDownloadToken) - Expires: At exactly 30 days after fulfillment
- Timeout: Downloads that don't complete within 30 days fail with "link expired"
Once expired, users can request a fresh export (subject to the 24-hour cooldown).
Export → import round-trip
goals.csv and habit_completions.csv inside the export bundle use the exact same column contract Data Import reads (ImportExport::CsvSchema) — you can take either file straight from an export and hand it back to the importer. This round-trip is a supported path, not an incidental compatibility, and is how the importer itself is integration-tested.
Technical Details
Backend Pipeline
Gdpr::RequestSelfServiceExportinteraction (rails_api/app/interactions/gdpr/request_self_service_export.rb):- Validates 24h cooldown per email
- Creates
GdprRequestrecord - Enqueues
Gdpr::BuildExportBundleJob
Gdpr::BuildExportBundleJobjob (rails_api/app/jobs/gdpr/build_export_bundle_job.rb):- Looks up user by
requestor_email - Calls
Gdpr::ExportBuilderto assemble ZIP - Uploads to GCS with timestamp-based path:
{public_id}/export-{iso8601}.zip - Generates 30-day signed URL with JWT (
Gdpr::ExportDownloadToken) - Updates
GdprRequeststatus tofulfilled - Sends email notification via
GdprMailer#export_ready - Wraps success in
Admin::Logged(tracks asgdpr_export_fulfilledaction) for admin visibility
- Looks up user by
Gdpr::ExportBuilderservice (rails_api/app/services/gdpr/export_builder.rb):- Queries user's complete data
- Generates
data.json(pretty-printed) - Generates per-table CSVs with headers
- Packages into ZIP as temp file
Email notification (
rails_api/app/views/gdpr_mailer/export_ready.html.erb):- Branded template with reference ID
- Download URL (truncated display, full link in href)
- Expiry date in user-friendly format ("April 5, 2026")
- Fallback: "If you did not request this, please reply"
Frontend Component
DataExportCard.vue (ionic_frontend/src/components/ui/DataExportCard.vue):
- Five-state machine (
idle | requesting | processing | ready | cooldown) - Emits
exportevent on button click → parent triggers mutation - Emits
downloadevent on download click → parent opens signed URL - Displays cooldown message with remaining hours
- Shows expiry date on ready state
- Fully accessible:
aria-liveon cooldown,aria-busyon requesting,role="alert"on processing
Privacy & Security
- One export per 24 hours per email — Prevents export spam and reduces load
- User authentication required — Exports only available to authenticated users
- Signed URLs — 30-day JWT tokens with automatic expiry (no indefinite access)
- No admin override — Users own the trigger; admins can only view request history via audit trail
- Scope-gated — Only user-owned and user-referenced data; no third-party user content
See Also
- Data import: Data Import — the importer that reads
goals.csv/habit_completions.csvback in, and the export→import round-trip - Data deletion: Data Deletion — the erasure side of the same
GdprRequestmodel - Roadmap: Phase 6 Growth & Virality
- Data models: docs/architecture/data-models.md
- Gamification: docs/features/gamification-engine.md
Last updated: 2026-09-15 — documented habit_completions.csv and the curated goals.csv/habit_completions.csv header sets, and the export→import round-trip (v4.64 Phase 7, OBJ-3827)
Prior update: 2026-09-12 — documented the share_attributions export exclusion alongside the existing share_events one (v4.56 Phase 5 referral attribution, OBJ-3683)