Skip to content

v4.64 — Import From Other Apps

A user switching from another habit tracker can upload a CSV of their goals, habits and check-in history, preview exactly what will be created, commit it, and keep their history intact — streaks recomputed, nothing leaked to a public surface, the file never persisted to storage.

Summary

Before this milestone, Objectuve had no import path at all — zero matches for Import anywhere in the Rails app, and the only CSV code in the whole codebase was the write-only GDPR export. That export itself couldn't be re-imported: it omitted habit_completions entirely (the single most valuable thing a switching user wants to keep) and derived its column headers from a model's full attribute set, so the contract silently changed with every migration.

Planning found the deeper risk was not "can we parse a CSV" but "can we parse a CSV safely." The existing per-row create paths — GoalTracking::AddGoal and GoalTracking::CheckInHabit — are built for one user action at a time and fire a cascade of side effects per call: background jobs, PostHog captures, feed fan-out to allies, and gamification rewards (XP, badges, streak-freeze tokens). Looping a multi-year history import through them unmodified would enqueue roughly 14,400 background jobs for a modest five-habit history, announce years of old activity to the user's allies as if it happened today, and mint XP and badges the user never actually earned in real time.

v4.64 ships a versioned CSV contract, a re-importable Objectuve export, a preview-then-commit pipeline that suppresses every one of those side effects and computes each streak exactly once, a ten-state frontend flow behind a feature flag, and a written, cited verdict on the four stretch-goal provider integrations (Habitica, Strides, Apple Health, Google Fit) the original issue asked about. The feature merged complete and correct at 0% rollout — this milestone is build-out, not yet a launch.

Goal

A user arriving from another habit tracker can upload a CSV of their goals, habits and check-in history, see exactly what will be created before anything is written, commit it, and end up with their history intact — streaks included — without a single row of that history leaking to any public surface and without the uploaded file ever being persisted to storage.

The secondary goal is symmetry: Objectuve's own data export becomes re-importable, which is what makes this feature testable end-to-end without depending on a third party.

Scope — What Shipped

  • ImportExport::CsvSchema (rails_api/app/services/import_export/csv_schema.rb) — the single source of truth for the two-file CSV contract (goals.csv, habit_completions.csv), consumed by both the parser and the exporter so no column name is declared twice.
  • A re-importable GDPR exportGdpr::ExportBuilder gained a habit_completions section and replaced its rows.first.keys header derivation with the curated schema column set for the goals/habit_completions sections, plus a stable external_id column on goals.csv (from public_id) that joins the two files on re-import.
  • DataImport model + parse/validate/preview — a new data_imports table, ImportExport::ParseCsv (2 MB / 5,000-row hard caps, never hands the upload to ActiveStorage), collision detection against existing goal names, and a beginDataImport mutation returning per-row errors and warnings with CSV line numbers.
  • The bulk commit pathImportExport::Commit + ImportExport::CommitJob create goals through GoalTracking::AddGoal in a new import mode (suppressing gamification enqueues, replacing the 'Goal created!' provenance event), insert check-ins directly with a deterministic client_event_id (idempotent re-import), and recompute each streak exactly once through a new award: keyword (default true) on User#update_streak! / Goal#update_habit_streak! — set false on the import path so no unearned XP, badges, or freeze tokens are minted from historical data.
  • A ten-state frontend flow at /settings/import (DataImportPage.vue + six step components), gated by data_import_enabled, with per-row error/warning presentation, skippable name-collision rows, a bounded in-flight progress poll, and a summary screen that suppresses the streak-celebration path entirely — an imported 3 → 180 streak jump never fires the normal celebration.
  • A written per-provider verdict (docs/product/import-provider-evaluation.md) on the four stretch-goal integrations named in the original issue.
  • User-facing docsdocs/features/data-import.md (new), plus reconciled docs/features/data-export.md and a sync-vs-migration cross-link in docs/features/connected-apps.md.

Phases

PhaseNameStatusPlansHighlights
1The CSV contract and a re-importable exportShipped1Versioned CsvSchema; GDPR export gains habit_completions.csv and stable headers; |-delimited recurrence_days encoding added after Roy caught the original fixture skipping the one non-scalar column.
2DataImport, parse, validate, previewShipped1Staged-preview pipeline, collision detection, hard size/row caps, zero ActiveStorage writes. Forced the DataImport(model)/ImportExport(namespace) split every later phase inherited.
3The bulk commit pathShipped1Side-effect-suppressed goal/check-in creation; streaks computed once via the new award: keyword — the milestone's core integrity mechanism.
4UI-SPEC — the import flowShipped1Ruled the dedicated /settings/import route, ten states, and the "no streak celebration on import" policy ahead of Phases 3 and 5.
5Import frontendShipped1Ten-state flow behind data_import_enabled (0%); found and fixed five UI-SPEC compliance gaps mid-build; routed two backend-shaped gaps to follow-up issues (OBJ-3874, OBJ-3875).
6Provider integration evaluationShipped1Cited verdict on Habitica, Strides, Apple Health, Google Fit — Google Fit ruled not viable (API sunsetting), the rest routed to Connected Apps or a future milestone.
7Docs reconciliationShipped1docs/features/data-import.md, reconciled data-export.md/connected-apps.md, roadmap item 45 ticked, CHANGELOG entry.

Key Decisions

  • Never persist the uploaded file. The app's only user-upload path (GraphQL multipart → ActiveStorage) writes to a public: true production GCS bucket. Rather than adding new private-bucket infrastructure, the CSV is parsed entirely in-request into staged rows and the Tempfile dies with the request — sidestepping the public-bucket problem instead of solving it.
  • Suppress gamification, don't fake it. A CSV must not mint what the user did not earn in real time. The award: keyword on update_streak!/update_habit_streak! is the mechanism: import mode still computes and stores the correct streak numbers, but skips every XP grant, badge trigger, and freeze-token issuance a normal check-in would fire.
  • DataImport is the model, ImportExport is the namespace. Phase 1 shipped DataImport::CsvSchema as a module; Phase 2 needed DataImport to be an ActiveRecord class, and the two constants can't coexist. The namespace was renamed to ImportExport mid-milestone, pinned by a regression spec, and every later phase's documentation carries the corrected paths.
  • No streak celebration on import. Desi's UI-SPEC ruled this before Phase 3 or 5 was built, not after a bug report — an imported goal that jumps a user's streak from 3 to 180 must not trigger the normal celebration animation. acknowledgeStreak() seeds the celebration composable's dedup ledger with the terminal streak value before the user cache is written.
  • CSV mapping first, device APIs later. Phase 6 found Habitica and Strides both already export CSV, so they're column-mapping work on the exact importer this milestone ships — not new integrations. Apple Health and Google Fit are device APIs with no matching CSV export; Google Fit's REST API is sunsetting by end of 2026, ruled not viable rather than deferred.

Requirements Coverage

48 / 48 requirements satisfied — no standalone REQUIREMENTS.md exists for this milestone; coverage is derived from the ROADMAP's own per-phase acceptance-criteria checklists (IMP-/CSV-/UI-/DOC- prefixes) and independently re-verified against the live master tree in the milestone audit.

CategoryCountStatus
Phase 1 — CSV contract / export5All satisfied
Phase 2 — parse / validate / preview7All satisfied
Phase 3 — bulk commit path13All satisfied
Phase 4 — UI-SPEC5All satisfied
Phase 5 — frontend10All satisfied
Phase 6 — provider evaluation4All satisfied
Phase 7 — docs4All satisfied

Full detail: v4.64-import-from-other-apps-MILESTONE-AUDIT.md on GitHub.

Outcomes

A user can now upload a CSV export from another habit tracker (or a re-exported Objectuve GDPR bundle), see exactly what will be created before anything is written, commit it, and end up with correctly recomputed streaks and no unearned reward state. Objectuve's own export is symmetric with its import for the first time, which is also what makes the importer testable end-to-end without a third-party dependency. The capability is complete and merged to master, but reaches no real user yet: data_import_enabled sits at 0% rollout in PostHog pending native file-picker device verification (OBJ-3874).

Tech Debt

  • (Phase 5, OBJ-3874) Native file-picker WebView selection is verified at code level only — never exercised on a physical iOS/Android device or simulator. Gates the flag moving past 0% rollout.
  • (Phases 1–3, OBJ-3875) Three backend diagnostics gaps reduce error-message quality without blocking correctness: two validation-reason strings don't distinguish cases the UI-SPEC's copy catalog wants distinguished, an unmatched goal_external_id at commit time is silently counted as a generic skip rather than surfaced as a row error, and only 2 of the UI-SPEC's 4 progress labels are backend-observable.
  • (Phase 7, OBJ-3884) Two non-blocking doc nits: a stale file-path citation in docs/product/import-provider-evaluation.md, and a UI copy label mismatch ("create anyway" vs. shipped "Add anyway").
  • (Milestone-wide) The ROADMAP's own Test Strategy named a Playwright smoke spec covering pick → preview → commit that was never shipped — confirmed absent from both ionic_frontend/tests/smoke-playwright/ and tests/e2e/specs/ at this close. Not a blocker at 0% rollout (Storybook play() + Vitest cover every component state), but worth closing before the flag ramps.
  • 1bb2d94d4ada74a7be72751f64f46d97cf061fef — Phase 1: CSV contract + re-importable export (PR #3201)
  • 5de6eb8fafc5ad5ab568e11c8505c8fbb4c2c000 — Phase 2: DataImport, parse/validate/preview (PR #3224)
  • dd2b7c84e9ab81315064bca5dfefc0e8d1701e70 — Phase 3: bulk commit path (PR #3237)
  • 29550f0a2965da97a885b0506d47615ea5702b3d — Phase 4: UI-SPEC (PR #3204)
  • e4fb2adf2923447bc29bd3a91d8cdab742d84ad3 — Phase 5: import frontend (PR #3248)
  • 7205e88a6a6b8a58e6d53e855e27d62110cddbb6 — Phase 6: provider integration evaluation (PR #3217)
  • 75bee71a9bcff605ec53349a1496294eafeb564a — Phase 7: docs reconciliation (PR #3259)

Last updated: 2026-09-15 Version: v4.64

Loading…