Skip to content

v4.14 — Activity Nexus — Connected Apps (Strava + Chess.com)

Link Strava or Chess.com once, and activity logged there checks off the matching Objectuve habit automatically — no more re-entering the same run or chess game twice.

Summary

Users who already track fitness on Strava or hold streaks on Chess.com had to re-enter the same activity as an Objectuve habit check-in — every redundant log-in is friction that breaks streaks, and it disproportionately pushes out the people most likely to thrive on the app if that friction weren't there. v4.14 closes that gap with a reusable Connected Apps integration framework carrying two live providers at launch: Strava (real-time, via a signature-verified webhook) and Chess.com (an hourly-gated daily poll of each user's public game archive). A confidence-scored mapping engine reviews the first connection with the user before anything links — auto-checking a high-confidence match or proposing a lower-confidence one, but never silently — and every check-in that came from a connected provider carries a visible "via Strava"/"via Chess.com" attribution pill in the activity feed. The feature is free, opt-in, one-way (Objectuve never writes back to either provider), and disconnecting a provider never touches past check-ins or streaks.

All six phases finished on the integration branch gsd/v4.14-activity-nexus-connected-apps by 2026-07-25, but the branch itself was never promoted to master — a known repo failure mode (CLAUDE.md's "Stranded-milestone-branch gotcha") that had already caught four other milestones. Penny's proposal (OBJ-1982, 2026-08-02) surfaced it a week ahead of the automated branch-promotion-guard's 14-day idle threshold. What followed was not a routine promotion: two of the three landing attempts broke master and had to be rolled back, each surfacing a real defect the branch's own testing hadn't caught (see Key Decisions). The milestone's code was correct in isolation; making it safe to actually merge took three rounds. Attempt 3 held clean through a 32-minute post-deploy Sentry watch window and is the version now on master.

This is a merge-only ship. The connected_apps_enabled feature flag stays at 0% general rollout — the feature is not visible to any real user yet. See Outcomes.

Goal

Ship a first-class Connected Apps surface that eliminates double-entry for users who already track activity elsewhere. A reusable integration framework carries two live providers at MVP — Strava (fitness, real-time via webhook) and Chess.com (streak play, daily poll) — that auto-check the matching Objectuve habits through a confidence-scored, user-reviewed mapping engine. Import is one-way only, the feature is free-tier, and every imported check-in is transparently attributed ("via Strava"). The framework is designed so Apple HealthKit / Google Health / Garmin drop in later without architectural rewrites.

.planning/milestones/v4.14-activity-nexus-connected-apps-ROADMAP.md

Scope — What Shipped

  • Integration framework — four models (IntegrationProvider, IntegrationConnection, HabitIntegrationMapping, IntegrationActivity), all PublicRecord + acts_as_paranoid; an abstract adapter contract (IntegrationProvider::Adapter); a connectedApps GraphQL read query; ActiveRecord::Encryption stood up for token storage (the A-ENCRYPT prerequisite, proven with a round-trip spec before any real token is stored).
  • Strava provider — OAuth 2.0 (activity:read_all scope only, in-app browser + deep-link callback), real-time ingestion via an HMAC-SHA256-signature-verified webhook (rails_api/app/controllers/webhooks/strava_controller.rb). No GPS/location/route data is requested or stored — only activity type, duration, distance, and timestamp.
  • Chess.com provider — connects with just a public username, no auth; Integrations::SyncChessActivityJob polls hourly and fetches each user's prior day of games once their local clock hits 2am.
  • Confidence-scored auto-mappingIntegrations::MapIncomingActivity (AUTO_CHECK_THRESHOLD = 0.85, PROPOSAL_THRESHOLD = 0.50) auto-checks a high-confidence match or proposes a lower-confidence one; a mid-milestone re-spec (OBJ-1712) fixed the confidence rule table so the propose band was actually reachable from real data, not just 0 or ≥0.90.
  • Settings UI — Connected Apps list (status badges, disconnect), first-connect mapping-review modal (accept/edit/dismiss/accept-all), Manage Mappings view, and a provider-branded attribution pill on feed items.
  • NewHabitToast — a completed-action toast for an auto-check-in, or a consent-first "want to link it?" toast for a proposal; every new mapping surfaces, none link silently (the PBC anti-dark-pattern non-negotiable for this milestone).
  • Legal — Termly Terms & Conditions "Third-Party Integrations" clause and Privacy Policy "connected apps" processing purpose, published 2026-07-25 (OBJ-1744).
  • Staged-ramp feature flagconnected_apps_enabled (PostHog), registered disabled at 0% rollout, fail-closed on both frontend and backend (OBJ-1745).
  • Docsdocs/features/connected-apps.md, docs/architecture/connected-apps-integration-framework.md, docs/operations/rollouts/v4.14-activity-nexus-connected-apps.md.

Phases

PhaseNameStatusPRs (on integration branch)Highlights
1Integration Framework FoundationShipped#17464 models, adapter contract, encrypted token round-trip proven
2Strava Integration (backend)Shipped#1752OAuth + signature-verified webhook
3Chess.com Integration (backend)Shipped#1753Hourly-gated daily poller
4Smart Auto-Mapping EngineShipped#1757, #1758 (OBJ-1712 re-spec)Confidence scoring; propose band made reachable
5Connected Apps Settings UIShipped#1760, #1766, #1774Settings surface, mapping modal, NewHabitToast (AN-AUTOMAP-SURFACE)
6ToS, Privacy & ReleaseShipped#1779, #1786Termly publish, connected_apps_enabled staged-ramp flag
Integration merge to master (OBJ-1982)Shipped, 3rd attempt#2003 (reverted), #2022 (reverted), #2034 (held)See Key Decisions

Key Decisions

  • Promotion required three attempts, not one — this is the honest record, not spin. Attempt 1 (PR #2003) merged with all required PR checks green but broke master's deploy pipeline: scripts/validate-secrets.sh is a deploy-time-only static check that never runs on pull_request, so a bare ENV.fetch('STRAVA_CLIENT_ID') with no default and no provisioned secret passed every PR gate and then failed the very next deploy. Fixed by defaulting three Strava env vars to nil (temporary — see Tech Debt) and re-landing.
  • A reverted merge commit doesn't remove its branch from master's ancestry — the re-land has to revert the revert. Attempt 1's revert (git revert -m 1) left gsd/v4.14-activity-nexus-connected-apps a permanent ancestor of master with its content absent; simply re-merging the same branch would have been a silent no-op. Every re-land in this saga reverted the prior revert instead.
  • Attempt 2 (PR #2022) passed every existing check and still shipped a live crash ~56 minutes after deploy. NewHabitToast mounted unconditionally in App.vue's global chrome — "the feature has a flag" turned out not to mean "the mount site is gated." A mood_reminder notification with details: nil serialized to the JSON string "null"; JSON.parse("null") returns null without throwing, so the existing try/catch around the parse didn't protect the very next line's property access, and the crash hit every logged-in user regardless of the connected_apps_enabled flag. Fixed with a v-if on the mount, a kind-based filter before parsing (not a wider try/catch), a same-shape fix to a second, deferred call site (useNotifications.ts) carrying the identical unguarded pattern, and two new regression tests.
  • Round 2's diagnosis explicitly enumerated every ungated surface before attempt 3, rather than re-asserting "flag-off ⇒ no user impact." Both prior failures traced to the same unverified premise. Attempt 3's package audited all three non-flag-gated frontend importers of v4.14 code and confirmed only one (NewHabitToast) was actually unsafe; the other two (AttributionPill call sites) were independently verified null-safe.
  • "Deploy went green" was demonstrably not sufficient — Sentry watch became an acceptance criterion, not an afterthought. Attempt 2 passed staging deploy clean and still crashed in production 56 minutes later. Attempt 3's release checklist added a required 30-minute post-deploy Sentry watch window before calling the ship done; it held clean at 32 minutes with zero new objectuve-client issues.
  • A pre-existing, unrelated staging Playwright failure was investigated rather than assumed safe. connected-apps-automap-toast.spec.ts failed on all three browsers on the attempt-3 deploy run — but this was the expected, correct consequence of NewHabitToast now actually being flag-gated (the staging smoke account isn't flagged into connected_apps_enabled), not a new regression. Spun off as a non-blocking follow-up (OBJ-2084) rather than treated as a rollback trigger.

Requirements Coverage

This milestone has no separate REQUIREMENTS.md — the ROADMAP's phase checklist and the three landing attempts' acceptance criteria are the requirements source of record. No formal MILESTONE-AUDIT.md was run; the promotion was a respec-under-fire across three attempts, each independently reviewed and approved (Roy) before shipping (Vicki), rather than a single fresh-phase-close audit.

Outcomes

The Connected Apps code is on master: the integration framework, both providers, the mapping engine, the Settings UI, and the legal/flag scaffolding all exist in the shipped codebase today. None of it is visible to real users yet. connected_apps_enabled is registered and fail-closed at 0% general rollout on both frontend and backend. Stage 1 (internal/QA) — the first ramp step — is blocked on Josh provisioning a Strava API application, the corresponding GCP Secret Manager secrets, and the Strava webhook subscription; until then, three of the five required Strava environment variables resolve to nil by design (see Tech Debt) and the live Strava/Chess.com round-trip has never been exercised in a deployed environment. AN-LEGAL (Termly ToS/Privacy Policy) is the one ramp precondition already satisfied.

Tech Debt

  • (Ops, blocking Stage 1) STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET, and STRAVA_REDIRECT_URI default to nil in rails_api/lib/integration_provider/strava.rb, marked # OBJ-1982 in-code, as a temporary deploy-gate fix — revert to a bare ENV.fetch (restoring validate-secrets.sh's zero-presence protection) once Josh provisions the Strava API app + GCP Secret Manager secrets + webhook subscription (B1–B3). See rollout playbook.
  • (Ops) Strava's free-tier rate-limit-increase application has not been submitted — not blocking Stage 1/2, but required before the Stage 3 public flip.
  • (Follow-up, OBJ-2084, Tess) connected-apps-connect.spec.ts and connected-apps-automap-toast.spec.ts need to be re-aligned with the current staging flag state — the former has a pre-existing, unrelated test-environment gap; the latter fails because the fix in this milestone correctly flag-gates the toast and the smoke account isn't flagged in.
  • (Follow-up, OBJ-2073, Codi) validate-secrets.sh still only runs at deploy time, not on pull_request — the exact gap attempt 1 fell through. Adding it as a PR-time check (path-gated) remains uncut after two rounds.
  • (Follow-up, OBJ-2082, Orion/Roy) A release-health gate idea from Dave's round-2 diagnosis: an automated new-issue-on-release Sentry signal that pauses or flags a deploy, instead of relying on a human noticing ~56 minutes later as happened in attempt 2.
  • (Process gap, flagged to Josh, unresolved) PR #2022's merge and its subsequent revert both happened outside the agent chain with zero issue-comment visibility — reconstructed after the fact from git log and Sentry, not caught in real time.
  • 506d03e39 — Attempt 3: flag-gate NewHabitToast + null-safe details_json (OBJ-1982)
  • 8e719f925 — Merge commit: PR #2034 into master, held clean through the Sentry watch window

Last updated: 2026-08-02

Loading…