Skip to content

v4.14 Activity Nexus — Connected Apps — Rollout Playbook

Phase: v4.14 milestone Phase 6 (AN-RELEASE) Feature flag: connected_apps_enabled (PostHog flag key, project 368400) Owner: Engineering (release) Last updated: 2026-08-03 Related: docs/product/completed/activity-nexus-prd.md, .planning/milestones/v4.14-activity-nexus-connected-apps-ROADMAP.md, docs/development/feature-flags.md, docs/operations/deployment.md, docs/milestones/v4.14-activity-nexus-connected-apps.md

Summary

v4.14 ships a reusable Connected Apps integration framework with two live providers — Strava (real-time, webhook-driven) and Chess.com (daily poll) — that auto-map incoming activity to existing habits via a confidence-scored, user-reviewed mapping engine. The whole surface is gated behind a single PostHog flag, connected_apps_enabled, evaluated identically by the frontend (isFeatureFlagEnabled('connected_apps_enabled'), ionic_frontend/src/lib/featureFlags.ts) and the backend (FeatureFlagService.enabled?('connected_apps_enabled', user:), rails_api/app/services/feature_flag_service.rb).

Flag introduced in this task (2026-07-24) — no earlier phase gated Connected Apps behind a flag; the surface was reachable by any authenticated user as of Phase 5 close. This task registered connected_apps_enabled in the registry, synced it to PostHog disabled at 0% rollout (node scripts/sync-posthog-feature-flags.mjs --apply), and wired the gate into every entry point below. No public flip has been performed — the flag stays at 0%/disabled until a human executes the Stage 3 step.

When the flag evaluates true for a user:

  • Frontend: /settings/connected-apps and /settings/connected-apps/:provider/mappings routes are reachable (ionic_frontend/src/router/index.ts beforeEnter guards); the "Manage connected apps" card renders on the Security settings tab (ionic_frontend/src/views/Settings.vue).
  • Backend: the connectedApps query resolves (rails_api/app/graphql/resolvers/integration_queries.rb); the connectStrava and connectChessCom mutations accept new connections (rails_api/app/graphql/mutations/integrations/connect_strava.rb, connect_chess_com.rb).

When the flag evaluates false (default, fail-closed for unconfigured users): the settings entry point is hidden, direct navigation to either route redirects to /settings, and the GraphQL surface raises FORBIDDEN ('Connected apps are not available yet'). Already-connected users are unaffected by anything in this task — nobody is connected yet, since the surface was ungated (and therefore untested against real data) prior to this flag existing.

Not gated (deliberately): the Strava webhook receiver (POST /webhooks/strava) and the Chess.com poller (Integrations::SyncChessActivityJob) are not flag-checked — they only ever act on connections that already exist, and no connection can be created while the flag is off. The lifecycle mutations (disconnectIntegrationConnection, pauseIntegrationConnection, resumeIntegrationConnection) are likewise ungated for the same reason: they only operate on a caller's own existing connection, which requires having connected while the flag was on for them.

Ramp plan

Three stages, matching the standard lifecycle in docs/development/feature-flags.md. Stage 3 (public) cannot open until OBJ-1744 (AN-LEGAL — Termly Privacy Policy + ToS updates covering Strava GPS/location data and Chess.com ingest) has landed and is live. This is a hard PBC/legal gate, not a technical one — do not flip Stage 3 based on technical readiness alone.

Stage 1 — Internal/QA (PostHog cohort filter)

  • PostHog config: PostHog → Feature Flags → connected_apps_enabled → Conditions → add a condition matching is_staff = true (same cohort property used by the v1.9 Goal Roadmap ramp) → 100% rollout for that condition only. Leave the flag's base active state and general rollout at the synced default (disabled / 0%) so no one outside the condition is affected.
  • Audience: internal/QA accounts only.
  • Goal: exercise the full connect → OAuth/username → mapping-review → feed-attribution flow against real (or sandbox) provider accounts before any real user sees it. This stage is also where the Tess live-integration pass (see Release checklist gate, below) must run — it is a hard prerequisite for Stage 2, not just Stage 3.
  • How to enable: requires a feature_flag:write-scoped PostHog personal API key. Per docs/development/feature-flags.md, always go through the PostHog UI or the sync script for flag state changes — never hand-roll a PATCH against the API.

Stage 2 — Limited rollout (percentage)

  • PostHog config: connected_apps_enabled → bump general rollout_percentage to a small slice (e.g. 5–10%) while keeping the Stage 1 internal/QA condition active.
  • Audience: a limited slice of the general population.
  • Goal: validate the mapping engine's confidence bands (Integrations::MapIncomingActivity — see OBJ-1712 re-spec) and the NewHabitToast surfacing guarantee against organic usage patterns before full exposure. Watch Sentry + the success criteria below.
  • Gate to advance: no rollback-signal trigger (below) fires for at least 48h at this rollout percentage.

Stage 3 — Public (100% rollout) — NOT performed in this task

  • Precondition (hard gate): OBJ-1744 (AN-LEGAL) shipped — Termly Privacy Policy + ToS explicitly cover Strava GPS/location data and Chess.com ingest, published and live.
  • Precondition (release checklist): Tess live-integration pass complete (see below), Strava rate-limit-increase application submitted (see below), and the 5 ENV vars provisioned in both staging and production (see below).
  • How to enable (a human executes this, not this task): PostHog → connected_apps_enabled → set general rollout_percentage to 100% (cohort conditions become redundant at this point and can be left in place or removed).
  • Audience: all Objectuve users.

Release checklist gate — required before the Stage 3 flip

ItemStatus as of 2026-08-03Owner
AN-LEGAL (Termly Privacy Policy + ToS)Landed — Termly Terms & Conditions "Third-Party Integrations" clause and Privacy Policy "connected apps" processing purpose published 2026-07-25 (OBJ-1744, PR #1786; see docs/company/legal-and-policies.md)Dori
Tess live-integration pass — live Strava OAuth connect + live Chess.com round-trip. Per Vicki's Phase 5 close note, neither provider's live path has ever been exercised in this environment (Phase 2/3 specs are WebMock-stubbed; Phase 5 FE specs mock the GraphQL layer). This is real-world OAuth/API behavior risk, not just code-path coverage.Not yet run — required pre-public-flip check, still blocked on the ENV vars row below.Tess
Strava rate-limit-increase applicationNot yet submitted — see next sectionRelease/Josh
5 Strava ENV vars provisioned in staging + productionNot yet provisioned — the outstanding blocker. Code now defaults 3 of the 5 to nil rather than raising (see the ENV var table below) so a merge to master is safe at 0% rollout, but Stage 1 (internal/QA) cannot meaningfully exercise the live Strava path, and the Tess row above cannot start, until these are provisioned. Requires Josh: Strava API app creation + GCP Secret Manager provisioning + webhook subscription registration — see next sectionRelease (GCP Secret Manager access required)

Only AN-LEGAL is satisfied as of this update. This checklist is the authoritative pre-public-flip gate for v4.14 — do not flip Stage 3 until every row is checked off.

Ops/env note — 5 required Strava ENV vars

Carried forward from Phase 2 (flagged in the ROADMAP's Phase 2 deviation note). All five are read via ENV.fetch in rails_api/lib/integration_provider/strava.rb and rails_api/app/controllers/webhooks/strava_controller.rb:

VariableRead atPurposeSecret?
STRAVA_CLIENT_IDstrava.rb:30,44OAuth app client id, sent on both the authorize redirect and the code→token exchangeNo (public per Strava's own OAuth flow) — but still not committed to source
STRAVA_CLIENT_SECRETstrava.rb:45OAuth app client secret, code→token exchangeYes
STRAVA_REDIRECT_URIstrava.rb:31Per-environment OAuth callback (.../integrations/strava/oauth_callback)No — differs per environment, plain value
Webhook verify token (STRAVA_WEBHOOK_VERIFY_TOKEN)strava_controller.rb:19Echoed back during Strava's hub.challenge subscription-validation handshake (GET /webhooks/strava)Yes (anyone who knows it can pass the handshake)
Webhook signing secret (STRAVA_WEBHOOK_SECRET)strava.rb:79HMAC-SHA256 key verifying the X-Strava-Signature header on inbound webhook events (POST /webhooks/strava)Yes

Current state — not yet wired: none of the five appear in deploy/service.staging.yaml, deploy/service.production.yaml, deploy/worker.*.yaml, or deploy/crono.*.yaml as of this update (2026-08-03). Temporary, as of the v4.14 re-land (OBJ-1982, 506d03e39): STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET, and STRAVA_REDIRECT_URI now default to nil (ENV.fetch('STRAVA_CLIENT_ID', nil), etc., rails_api/lib/integration_provider/strava.rb) instead of a bare ENV.fetch that raises KeyError. This was a deploy-gate fix, not an ops readiness change: scripts/validate-secrets.sh — a deploy-time-only static check — treats a bare ENV.fetch with no default as a required secret and fails the merge if it's absent from every deploy manifest; since these three vars are unreachable while connected_apps_enabled is at 0% (no connect flow can run), a nil default is safe today and turns an unreachable-code deploy-gate failure into an unreachable-code no-op. This is not a substitute for provisioning B1–B3 below — a nil client id/secret simply means the connect flow fails at Strava's API instead of with a Ruby KeyError, and the code carries an explicit OBJ-1982 comment marking the default as temporary, to be reverted to a bare ENV.fetch once B1–B3 land (restoring OBJ-1340's zero-presence protection for these vars like every other required secret). The two ENV.fetch(..., nil) calls that predate this task (verify token, webhook secret) were already nil-defaulted and fail closed (handshake/signature rejected) rather than raising — unchanged, and correct as a permanent default, not a temporary one.

Action item — before Stage 1 (internal/QA) can meaningfully exercise the live Strava path:

  1. Create a Strava API application at https://www.strava.com/settings/api (or reuse an existing sandbox app) to obtain a client id/secret pair. Staging and production should use separate Strava API apps, following this repo's established never-share-a-live-resource-across-environments pattern (see the GCS_BUCKET_NAME isolation precedent in deploy/service.staging.yaml).
  2. Provision GCP Secret Manager secrets (naming convention matches existing entries like clerk-secret-key, rails-master-key in deploy/service.production.yaml): strava-client-id, strava-client-secret, strava-webhook-verify-token, strava-webhook-secret — one set per environment.
  3. Add secretKeyRef entries for the four secrets above, and a plain per-environment value: entry for STRAVA_REDIRECT_URI (mirroring the GCS_BUCKET_NAME plain-value pattern), to deploy/service.staging.yaml and deploy/service.production.yaml. STRAVA_REDIRECT_URI should resolve to https://staging.api.objectuve.com/integrations/strava/oauth_callback (staging) and https://api.objectuve.com/integrations/strava/oauth_callback (production), per the environment/domain table in docs/operations/deployment.md.
  4. Register the webhook subscription with Strava (POST https://www.strava.com/api/v3/push_subscriptions with client_id, client_secret, callback_url = https://<env-api-host>/webhooks/strava, and verify_token = the same value stored in STRAVA_WEBHOOK_VERIFY_TOKEN) once the callback endpoint is reachable in that environment. There is no in-repo script for this step — it's a one-time manual curl/Postman call per environment against Strava's API.
  5. This provisioning work requires GCP Secret Manager write access, which this task does not have — hand off to whoever holds that access (same actor who provisioned clerk-secret-key et al.).

Strava rate-limit-increase application note

Strava's default free-tier limits (per PRD §9, Open Questions): 200 requests / 15 min, 2,000 requests / day, enforced per API application (not per connected user). Per the PRD's own risk analysis, this becomes a real constraint at roughly 5,000 connected users — comfortably above Stage 1/2 rollout volumes, but a hard blocker if reached mid-ramp without an increase in place.

Action item — pre-public (Stage 3) action item, not blocking Stage 1/2: apply for an increased rate limit via Strava's API settings (https://www.strava.com/settings/api → request a limit increase) before the Stage 3 flip. Strava's application form asks for expected request volume — base the estimate on webhook event volume (one inbound event per new/updated Strava activity per connected user, real-time push, not polled — see A3 in the PRD) projected against the Stage 2 rollout's observed connected-user count, extrapolated to 100%. No such application has been submitted as of this task.

Graceful degradation already in place: per the PRD's risk table and Integrations::AutoCheckIn, a rate-limit failure logs a warning + Sentry alert and does not queue check-ins retroactively — it does not crash or silently corrupt data. This means exceeding the limit degrades to "some activities don't get imported until the next webhook event," not an outage, which lowers the urgency of the application relative to the legal gate but does not remove it from the pre-public checklist.

Rollback procedure

  1. Disable the flag (PostHog → Feature Flags → connected_apps_enabled → set rollout to 0% / active: false). Effect is global within ~30s; both frontend and backend evaluate fail-closed. Never PATCH the PostHog API directly for this — use the UI or the sync script's --promote/manual toggle, per the credential-gotcha rule in docs/development/feature-flags.md.
  2. Verify: confirm /settings/connected-apps redirects to /settings for a non-cohort test account, and that connectStrava/connectChessCom return a FORBIDDEN GraphQL error.
  3. Disabling the flag does not touch already-created IntegrationConnection/IntegrationActivity/HabitIntegrationMapping rows or already-checked-in GoalEvents — it only stops new connects and hides the surface. If a rollback is data-related (e.g. bad auto-check-ins), that's a per-mapping auto_check_enabled toggle or manual data fix, not a flag flip.
  4. Notify Slack #engineering of the rollback and root cause.

Rollback signal triggers

  • Sentry error rate on Integrations::AuthorizeConnection, Integrations::ConnectChess, Strava::IngestionJob, or Integrations::SyncChessActivityJob exceeds 1% of invocations over a 15-minute window.
  • Any confirmed report of a mis-mapped or silently-checked auto-check-in (the PBC non-negotiable — proposals must never silently check a habit).
  • Strava webhook signature-verification failure rate spikes (possible spoofing attempt or a misconfigured/rotated STRAVA_WEBHOOK_SECRET).
  • Strava API rate-limit errors observed in Sentry before the rate-limit-increase application (above) has been approved.

Appendix — flag key + observability quick reference

SurfaceWrapperReads from
FrontendisFeatureFlagEnabled('connected_apps_enabled') (ionic_frontend/src/lib/featureFlags.ts)PostHog client (posthog.isFeatureEnabled)
BackendFeatureFlagService.enabled?('connected_apps_enabled', user:) (rails_api/app/services/feature_flag_service.rb)PostHog server-side via POSTHOG_CLIENT
Dev overridelocalStorage.setItem('objectuve_connected_apps_enabled_override', 'on' | 'off' | 'auto')local browser (development mode only)
ObservabilityWhere
Sentry — Strava/Chess.com integration errorsProject filter: transaction: any of AuthorizeConnection, ConnectChess, IngestionJob, SyncChessActivityJob, MapIncomingActivity, AutoCheckIn
PostHog — flag statehttps://us.posthog.com/project/368400/feature_flagsconnected_apps_enabled
Lograge — Rails request logsGCP Cloud Logging: jsonPayload.controller="GraphqlController" (connect mutations) or jsonPayload.controller="Webhooks::StravaController"

Loading…