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 export —
Gdpr::ExportBuildergained ahabit_completionssection and replaced itsrows.first.keysheader derivation with the curated schema column set for thegoals/habit_completionssections, plus a stableexternal_idcolumn ongoals.csv(frompublic_id) that joins the two files on re-import. DataImportmodel + parse/validate/preview — a newdata_importstable,ImportExport::ParseCsv(2 MB / 5,000-row hard caps, never hands the upload to ActiveStorage), collision detection against existing goal names, and abeginDataImportmutation returning per-row errors and warnings with CSV line numbers.- The bulk commit path —
ImportExport::Commit+ImportExport::CommitJobcreate goals throughGoalTracking::AddGoalin a new import mode (suppressing gamification enqueues, replacing the'Goal created!'provenance event), insert check-ins directly with a deterministicclient_event_id(idempotent re-import), and recompute each streak exactly once through a newaward:keyword (defaulttrue) onUser#update_streak!/Goal#update_habit_streak!— setfalseon 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 bydata_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 imported3 → 180streak 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 docs —
docs/features/data-import.md(new), plus reconcileddocs/features/data-export.mdand a sync-vs-migration cross-link indocs/features/connected-apps.md.
Phases
| Phase | Name | Status | Plans | Highlights |
|---|---|---|---|---|
| 1 | The CSV contract and a re-importable export | Shipped | 1 | Versioned 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. |
| 2 | DataImport, parse, validate, preview | Shipped | 1 | Staged-preview pipeline, collision detection, hard size/row caps, zero ActiveStorage writes. Forced the DataImport(model)/ImportExport(namespace) split every later phase inherited. |
| 3 | The bulk commit path | Shipped | 1 | Side-effect-suppressed goal/check-in creation; streaks computed once via the new award: keyword — the milestone's core integrity mechanism. |
| 4 | UI-SPEC — the import flow | Shipped | 1 | Ruled the dedicated /settings/import route, ten states, and the "no streak celebration on import" policy ahead of Phases 3 and 5. |
| 5 | Import frontend | Shipped | 1 | Ten-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). |
| 6 | Provider integration evaluation | Shipped | 1 | Cited 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. |
| 7 | Docs reconciliation | Shipped | 1 | docs/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: trueproduction GCS bucket. Rather than adding new private-bucket infrastructure, the CSV is parsed entirely in-request into staged rows and theTempfiledies 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 onupdate_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. DataImportis the model,ImportExportis the namespace. Phase 1 shippedDataImport::CsvSchemaas a module; Phase 2 neededDataImportto be anActiveRecordclass, and the two constants can't coexist. The namespace was renamed toImportExportmid-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.
| Category | Count | Status |
|---|---|---|
| Phase 1 — CSV contract / export | 5 | All satisfied |
| Phase 2 — parse / validate / preview | 7 | All satisfied |
| Phase 3 — bulk commit path | 13 | All satisfied |
| Phase 4 — UI-SPEC | 5 | All satisfied |
| Phase 5 — frontend | 10 | All satisfied |
| Phase 6 — provider evaluation | 4 | All satisfied |
| Phase 7 — docs | 4 | All 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_idat 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/andtests/e2e/specs/at this close. Not a blocker at 0% rollout (Storybookplay()+ Vitest cover every component state), but worth closing before the flag ramps.
Related Artifacts
- Roadmap: v4.64-import-from-other-apps-ROADMAP.md
- Milestone Audit: v4.64-import-from-other-apps-MILESTONE-AUDIT.md
- Phase artifact (UI-SPEC): .planning/phases/v4.64-obj-3824-import-flow/
- Feature docs: Data Import · Data Export · Connected Apps · Import Provider Evaluation
- Source issue: OBJ-1130 ("45. Import from Other Apps"),
docs/product/roadmap.md§ Phase 6 item 45 - Git tag: none yet — merged behind
data_import_enabledat 0% rollout, rides the next production tag once a rollout decision is made - Merge PRs: #3201 · #3224 · #3237 · #3204 · #3248 · #3217 · #3259
Related Commits
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