Skip to content

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:

  1. 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."
  2. Creates a GDPR request record — Backend creates a GdprRequest with status received and metadata { source: 'in_app_self_service' }.
  3. Enqueues the export jobGdpr::BuildExportBundleJob is dispatched to process asynchronously.

Users see five possible states on the card:

StateTriggerUIDuration
IdleInitial state or after cooldown expires"Export my data" button enabledN/A
CooldownUser has made a request within last 24hButton disabled; shows "You can request another export in X hours"24 hours
RequestingMutation accepted; job queued"Preparing your export..." spinner + disabled button~1–5 seconds
ProcessingJob running; user notified"Your export is on its way. We'll email you when it's ready."~5–60 seconds (varies by data volume)
ReadyJob complete; email sent"Your export is ready. Download expires [date]." + Download button30 days

What's Included

The export is a single ZIP archive containing two representations of the same data:

  1. data.json — Machine-readable full dump of all tables (JSON array format).
  2. Per-table CSVs — Human-friendly spreadsheet view of each table.

Exported Tables

TableDescriptionNotes
userUser profileIncludes username, email, first/last name, account creation/update dates, verification status
goalsAll 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_completionsHabit check-in historydata.json carries every column; habit_completions.csv uses the same curated set the importer reads: goal_external_id, completed_date, note.
goal_eventsGoal updates and milestonesProgress logs, event descriptions, timestamps, and media references
mood_logsDaily mood entriesMood score, date, and timestamp
user_actionsGamification eventsBadge unlocks, level-ups, streak milestones, and XP gains
milestonesGoal sub-goalsMilestone title, target date, and completion status
user_devicesRegistered devicesDevice tokens, platform (iOS/Android/web), and registration date
community_membershipsCommunity participationCommunity ID, role, and join date
ai_employee_memoriesAI coaching historyMemories recorded about the user by Coach, for personalization
admin_actionsAdministrative logsActions taken by admins (e.g., moderation, user support)
feedback_postsUser feedback submissionsCommunity feedback board posts authored by the user
feedback_commentsFeedback discussionComments 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 via goals/user_actions/etc. Gdpr::ExportBuilder (rails_api/app/services/gdpr/export_builder.rb) has no share_events section
  • share_attributions — same rationale and same silent exclusion as share_events above: it's a private referral-attribution record (which ShareEvent a signup is attributed to, and when), not authored content. Gdpr::ExportBuilder has no share_attributions section 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

  1. Gdpr::RequestSelfServiceExport interaction (rails_api/app/interactions/gdpr/request_self_service_export.rb):

    • Validates 24h cooldown per email
    • Creates GdprRequest record
    • Enqueues Gdpr::BuildExportBundleJob
  2. Gdpr::BuildExportBundleJob job (rails_api/app/jobs/gdpr/build_export_bundle_job.rb):

    • Looks up user by requestor_email
    • Calls Gdpr::ExportBuilder to assemble ZIP
    • Uploads to GCS with timestamp-based path: {public_id}/export-{iso8601}.zip
    • Generates 30-day signed URL with JWT (Gdpr::ExportDownloadToken)
    • Updates GdprRequest status to fulfilled
    • Sends email notification via GdprMailer#export_ready
    • Wraps success in Admin::Logged (tracks as gdpr_export_fulfilled action) for admin visibility
  3. Gdpr::ExportBuilder service (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
  4. 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 export event on button click → parent triggers mutation
  • Emits download event on download click → parent opens signed URL
  • Displays cooldown message with remaining hours
  • Shows expiry date on ready state
  • Fully accessible: aria-live on cooldown, aria-busy on 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

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)

Loading…