Google Health API Connection — Project Plan

Complexity: High Integration: Google health/fitness cloud sync for health, exercise, and activity data Primary goal: Sync user-authorized Google health/fitness data into Lifehub, create activity logs automatically from exercise sessions, and auto-complete related habits when synced activity proves the habit was done.


⚠️ Corrections Applied During Implementation Review

The implementation targets the real Google Health API at https://health.googleapis.com/v4 (the next-generation Fitbit Web API, documented at https://developers.google.com/health). This section captures deviations from the original plan after reviewing the live API surface.

  • API surface: Base URL https://health.googleapis.com/v4; all paths use the form /users/{userId}/dataTypes/{dataType}/dataPoints where {dataType} is a kebab-case slug (exercise, steps, sleep, heart-rate, etc.).
  • No generated Ruby client: google-apis-health_v4 is not published on rubygems. The implementation calls the REST API directly with Net::HTTP and uses only the real googleauth gem for OAuth2 token refresh.
  • Real OAuth scopes: https://www.googleapis.com/auth/googlehealth.* (e.g. .activity_and_fitness.readonly, .health_metrics_and_measurements.readonly, .sleep.readonly, .profile.readonly).
  • Webhooks are first-class: The API exposes subscriber resources at /v4/{parent=projects/*}/subscribers. The webhook controller and a Health::Google::SubscriberManager register/teardown subscribers.
  • Encryption: Active Record encryption was not configured in this app before this feature. The implementation adds encrypted-attribute support and the operator must generate keys before connecting any real user.
  • Indexes: Cross-user uniqueness — [provider, external_id] becomes [user_id, provider, external_id] so two users importing the same external id do not collide.
  • User association: standardized on has_many :health_connections (one row per provider per user), even though v1 only has one provider.

See next-steps.md for the operational decisions the implementer must complete.


Executive Summary

Lifehub should integrate with Google Health API as a first-class health data provider. Google Health API is the right choice for this Rails app because it is a cloud/server API, uses Google OAuth 2.0, supports background syncs and webhooks, and exposes fitness and health data from Google-backed device/app sources.

This project should create a new Health domain in Lifehub, rather than forcing every data point into ActivityLog. ActivityLog should remain the user-facing sports/activity record, while the new Health layer stores provider connections, raw normalized health records, daily summaries, sync state, and mapping rules.

The integration should support three automatic outcomes:

  1. Automatic syncs: scheduled jobs plus Google Health webhook-triggered incremental syncs.
  2. Automatic activity logs: synced exercise sessions create or update ActivityLog records, with support for matched existing sports, auto-created provider sports, or logs without a Lifehub sport if needed.
  3. Automatic habit completion: habits configured as activity/sport-driven can be checked automatically when imported activity meets the habit rule.

Product Goals

  • Let users connect Google Health from Lifehub settings or a new Health page.
  • Import exercise sessions, steps, calories, distance, sleep, heart rate, resting heart rate, HRV, VO2 max, weight, body fat, SpO2, and other supported data types incrementally.
  • Create ActivityLog records from Google Health exercise sessions without duplicates.
  • Preserve provider metadata so Lifehub can reconcile updates/deletions from Google Health.
  • Let users decide whether imported activities should create custom sports automatically.
  • Let users map Google Health exercise types to existing Lifehub sports.
  • Auto-complete activity-related habits when imported exercise or activity totals satisfy the habit rule.
  • Keep all user-owned and simple: every record belongs directly to User.
  • Avoid adding new gems unless explicitly approved later.

Non-Goals For The First Release

  • No Health Connect Android integration in this phase.
  • No Google Fit integration (Google Fit is the legacy Fitbit Web API predecessor; we target the new Google Health API at health.googleapis.com/v4).
  • No Fitbit Web API integration.
  • No medical diagnosis, clinical interpretation, or medical advice.
  • No writing Lifehub data back to Google Health in v1.
  • No native mobile app.
  • No new app/services/ directory. Use namespaced model classes under app/models/health/.

Current Lifehub Context

Lifehub already has:

  • User has_many :sports
  • User has_many :activity_logs
  • User has_many :habits
  • ActivityLog belongs_to :user and belongs_to :sport
  • ActivityLog fields: activity_type, date, duration_minutes, intensity, notes, sport_data, sport_id, user_id
  • Activity goals auto-refresh after ActivityLog save/destroy.
  • Habit goals auto-refresh after HabitCompletion save/destroy.
  • Manual activity creation awards sports XP in ActivityLogsController#create.
  • Manual habit toggling awards habit XP in HabitsController#toggle.

Important implication: background imports should not blindly reuse manual controller behavior. Provider syncs need their own idempotent XP/habit/goal rules.


High-Level Architecture

Google OAuth consent
        |
        v
Health::Connection
        |
        v
Health::SyncJob / Health::BackfillJob / Health::WebhookSyncJob
        |
        v
Google Health API client
        |
        v
Health::DataPoint + Health::DailySummary + Health::ExerciseSession
        |
        +--> ActivityLog auto-create/update
        |
        +--> Habit auto-completion rules
        |
        +--> Goals, analytics, gamification refresh

The Health layer should be the canonical synced provider store. ActivityLog should be a projection for exercise sessions that belong in the existing sports area.


Proposed Data Model

Health::Connection

Stores the user’s Google Health authorization and sync state.

Suggested table: health_connections

create_table :health_connections do |t|
  t.references :user, null: false, foreign_key: true
  t.string :provider, null: false, default: "google_health"
  t.string :status, null: false, default: "pending"
  t.string :google_user_id
  t.text :access_token_ciphertext
  t.text :refresh_token_ciphertext
  t.datetime :access_token_expires_at
  t.json :scopes, default: []
  t.json :settings, default: {}
  t.datetime :last_synced_at
  t.datetime :last_webhook_at
  t.datetime :disconnected_at
  t.text :last_error
  t.timestamps
end

add_index :health_connections, [:user_id, :provider], unique: true
add_index :health_connections, [:provider, :google_user_id]
add_index :health_connections, :status

Model notes:

  • belongs_to :user
  • enum provider: { google_health: "google_health" }
  • enum status: { pending, connected, sync_failed, disconnected, revoked }
  • Store tokens encrypted. Prefer Rails encryption if configured. If not, use Rails encrypted credentials for app secrets and add Active Record encryption before implementation.
  • settings should include user preferences:
    • auto_create_activity_logs: true
    • auto_create_sports: true
    • award_xp_for_imports: false
    • sync_data_types: [...]
    • activity_log_visibility: "enabled"

Health::DataPoint

Stores normalized provider data for all non-exercise health metrics and low-level interval/sample data.

Suggested table: health_data_points

create_table :health_data_points do |t|
  t.references :user, null: false, foreign_key: true
  t.references :health_connection, null: false, foreign_key: true
  t.string :provider, null: false, default: "google_health"
  t.string :external_id, null: false
  t.string :data_type, null: false
  t.string :record_kind, null: false
  t.datetime :started_at
  t.datetime :ended_at
  t.date :date, null: false
  t.decimal :value, precision: 15, scale: 4
  t.string :unit
  t.string :source_name
  t.string :source_device
  t.json :metadata, default: {}
  t.json :raw_payload, default: {}
  t.datetime :deleted_at
  t.timestamps
end

add_index :health_data_points, [:provider, :external_id], unique: true
add_index :health_data_points, [:user_id, :data_type, :date]
add_index :health_data_points, [:user_id, :date]

Model notes:

  • record_kind values: sample, interval, daily, summary.
  • data_type examples: steps, distance, heart_rate, daily_resting_heart_rate, sleep, weight, oxygen_saturation, vo2_max.
  • Store raw payload for traceability, but dashboards should read normalized fields.

Health::ExerciseSession

Stores exercise/workout sessions from Google Health. These are candidates for automatic ActivityLog creation.

Suggested table: health_exercise_sessions

create_table :health_exercise_sessions do |t|
  t.references :user, null: false, foreign_key: true
  t.references :health_connection, null: false, foreign_key: true
  t.references :activity_log, foreign_key: true
  t.references :sport, foreign_key: true
  t.string :provider, null: false, default: "google_health"
  t.string :external_id, null: false
  t.string :exercise_type, null: false
  t.string :title
  t.datetime :started_at, null: false
  t.datetime :ended_at, null: false
  t.date :date, null: false
  t.integer :duration_minutes
  t.decimal :distance_meters, precision: 12, scale: 2
  t.decimal :calories_kcal, precision: 12, scale: 2
  t.integer :average_heart_rate
  t.integer :max_heart_rate
  t.string :source_name
  t.string :source_device
  t.json :route_summary, default: {}
  t.json :metrics, default: {}
  t.json :raw_payload, default: {}
  t.datetime :deleted_at
  t.timestamps
end

add_index :health_exercise_sessions, [:provider, :external_id], unique: true
add_index :health_exercise_sessions, [:user_id, :date]
add_index :health_exercise_sessions, [:user_id, :exercise_type]

Model notes:

  • activity_log_id links the synced session to its Lifehub projection.
  • sport_id is optional so unmapped provider activities are supported.
  • Updates from Google Health should update this record first, then update the linked activity log.

Health::DailySummary

Stores daily rollups for fast dashboards and all-time analytics.

Suggested table: health_daily_summaries

create_table :health_daily_summaries do |t|
  t.references :user, null: false, foreign_key: true
  t.date :date, null: false
  t.integer :steps
  t.decimal :distance_meters, precision: 12, scale: 2
  t.decimal :active_minutes, precision: 10, scale: 2
  t.decimal :active_zone_minutes, precision: 10, scale: 2
  t.decimal :calories_kcal, precision: 12, scale: 2
  t.integer :resting_heart_rate
  t.decimal :sleep_minutes, precision: 10, scale: 2
  t.decimal :weight_kg, precision: 10, scale: 3
  t.decimal :body_fat_percentage, precision: 6, scale: 3
  t.decimal :vo2_max, precision: 6, scale: 2
  t.json :metrics, default: {}
  t.datetime :synced_at
  t.timestamps
end

add_index :health_daily_summaries, [:user_id, :date], unique: true

Health::SportMapping

Maps provider exercise types to Lifehub sports.

Suggested table: health_sport_mappings

create_table :health_sport_mappings do |t|
  t.references :user, null: false, foreign_key: true
  t.references :sport, foreign_key: true
  t.string :provider, null: false, default: "google_health"
  t.string :provider_exercise_type, null: false
  t.string :activity_type, null: false, default: "custom"
  t.boolean :auto_create_sport, default: true, null: false
  t.json :settings, default: {}
  t.timestamps
end

add_index :health_sport_mappings, [:user_id, :provider, :provider_exercise_type], unique: true

Mapping examples:

Google Health exercise type Lifehub sport type Default behavior
running running Link to existing Corrida, or create Google Health Running
cycling cycling Link/create sport
swimming swimming Link/create sport
strength_training gym Link/create sport
unknown/new provider type custom Create custom sport or leave sport_id blank depending on user setting

Health::HabitRule

Defines which habits can be completed by imported health data.

Suggested table: health_habit_rules

create_table :health_habit_rules do |t|
  t.references :user, null: false, foreign_key: true
  t.references :habit, null: false, foreign_key: true
  t.string :provider, null: false, default: "google_health"
  t.string :trigger_type, null: false
  t.string :data_type
  t.string :exercise_type
  t.integer :sport_id
  t.decimal :minimum_value, precision: 12, scale: 2
  t.string :unit
  t.boolean :active, default: true, null: false
  t.json :settings, default: {}
  t.timestamps
end

add_index :health_habit_rules, [:user_id, :habit_id]
add_index :health_habit_rules, [:user_id, :trigger_type]

Rule examples:

  • Complete Exercício físico if any exercise session has duration >= 20 minutes.
  • Complete Caminhada 30 min if steps >= 6,000 or walking session duration >= 30 minutes.
  • Complete Beber 2L de Água if hydration volume >= 2 liters, if we later sync hydration.
  • Complete a specific gym habit if exercise type is strength_training and duration >= 45 minutes.

Activity Log Creation Strategy

Why Activity Logs Should Be A Projection

Google Health exercise sessions can be updated, corrected, deleted, or reconciled after initial sync. Lifehub should keep provider sessions in Health::ExerciseSession and then project them into ActivityLog.

Add Provider Fields To ActivityLog

To make imports idempotent, add provider identity fields to activity_logs:

add_column :activity_logs, :source_provider, :string
add_column :activity_logs, :external_id, :string
add_column :activity_logs, :started_at, :datetime
add_column :activity_logs, :ended_at, :datetime
add_column :activity_logs, :imported, :boolean, default: false, null: false
add_column :activity_logs, :imported_at, :datetime
add_index :activity_logs, [:source_provider, :external_id], unique: true

sport_id is currently nullable in the schema, but the model declares belongs_to :sport, which makes it required by default in modern Rails. To support unmapped Google Health activity, update it to:

belongs_to :sport, optional: true

The UI should handle log.sport.nil? by showing the provider activity label from sport_data.

Mapping Algorithm

When an exercise session is synced:

  1. Upsert Health::ExerciseSession by [provider, external_id].
  2. Skip projection if the user disabled automatic activity logs.
  3. Find a Health::SportMapping for the provider exercise type.
  4. If mapping has sport_id, use that sport.
  5. If no mapping and auto_create_sports is enabled:
    • Find an existing sport by normalized name/type, or create one owned by the user.
    • Create a mapping so future sessions reuse the same sport.
  6. If no mapping and auto-create is disabled:
    • Create ActivityLog with no sport, activity_type: "custom", and provider details in sport_data.
  7. Upsert ActivityLog by [source_provider, external_id].
  8. Set activity_log_id on the Health::ExerciseSession.
  9. Refresh goals and habit rules.

Activity Type Conversion

Lifehub activity types are currently:

running tennis cycling swimming soccer martial_arts yoga gym custom

Google Health may produce activity types that do not exist in Lifehub. Conversion should be conservative:

  • Known type maps to matching Lifehub enum.
  • Strength/weights maps to gym.
  • Everything else maps to custom.
  • Original provider type is always stored in sport_data["provider_exercise_type"].

Suggested sport_data For Imported Logs

{
  "provider" => "google_health",
  "provider_exercise_type" => "strength_training",
  "provider_title" => "Upper Body Workout",
  "source_name" => "Pixel Watch",
  "source_device" => "Google Pixel Watch 3",
  "distance_meters" => 5200.0,
  "calories_kcal" => 430.0,
  "average_heart_rate" => 142,
  "max_heart_rate" => 178,
  "synced_at" => Time.current.iso8601
}

Habit Auto-Completion Strategy

Why This Needs Explicit Rules

Not every imported activity should complete every fitness habit. A generic workout might complete Exercício físico, but it should not automatically complete Treino de pernas unless the user mapped that habit to the right exercise type or sport.

Rule Types

Use Health::HabitRule#trigger_type values:

  • any_exercise_session
  • exercise_type_duration
  • sport_duration
  • steps_total
  • distance_total
  • active_minutes_total
  • calories_total
  • hydration_total

Completion Flow

After syncing a session or daily summary:

  1. Find active rules for the user.
  2. Evaluate rules for the affected date.
  3. If a rule passes, call habit.complete_on!(date, value:, notes:).
  4. Add notes such as Completed from Google Health: 42 min running.
  5. Recalculate streaks through the existing habit completion flow.
  6. Keep idempotency through the unique [habit_id, date] index on habit_completions.

XP Policy

Recommended default for v1: do not award manual habit or sports XP for imported historical records.

Reason:

  • Initial backfills could create hundreds of activity logs and habit completions.
  • Existing manual XP methods are written for user-triggered actions.
  • Imported health data can update after the fact.

Suggested policy:

  • Backfills: no XP.
  • Same-day automatic sync: optional XP, disabled by default.
  • If enabled, award XP once per imported activity/habit per day using source-aware idempotency.
  • Consider adding xp_awarded_at to health_exercise_sessions and health_habit_rules outcome records later if XP becomes product-critical.

Sync Design

Sync Modes

  1. Initial backfill
    • Runs after successful OAuth connection.
    • Default range: last 90 days.
    • Optional user-triggered all-time backfill.
    • Syncs in small date windows to avoid rate limits.
  2. Scheduled sync
    • Runs every 1-3 hours through Solid Queue recurring jobs.
    • Syncs from last_synced_at - overlap_window to now.
    • Use a 24-hour overlap to catch provider corrections.
  3. Webhook sync
    • Google Health API supports subscriber endpoints.
    • Webhook marks affected connection/user as dirty and enqueues Health::WebhookSyncJob.
    • Webhooks should not do heavy API work inline.
  4. Manual sync
    • A button in the Health page: Sync now.
    • Rate-limited by connection, e.g. no more than once every 5 minutes.

Data Types For V1

Start with these Google Health API data types:

  • exercise
  • steps
  • distance
  • active-minutes
  • active-zone-minutes
  • total-calories
  • sleep
  • heart-rate
  • daily-resting-heart-rate
  • heart-rate-variability
  • vo2-max
  • weight
  • body-fat
  • oxygen-saturation

Add later:

  • hydration
  • nutrition
  • respiratory rate
  • skin temperature
  • blood pressure
  • blood glucose

Sync Windows

  • Exercise sessions: list/reconcile by date range.
  • Daily rollups: dailyRollUp for supported aggregate types.
  • Samples like heart rate: import summaries first; only import granular records when needed.
  • Avoid storing excessive intraday data until a dashboard needs it.

Idempotency Requirements

  • Every provider record must have a stable external_id.
  • Every upsert must be scoped by [provider, external_id].
  • Activity logs must use [source_provider, external_id].
  • Backfills must be safe to rerun.
  • Webhook jobs must be safe to retry.

Deletion Handling

  • If Google Health reports a deleted record, mark local health records deleted_at.
  • For linked ActivityLog, either:
    • soft-delete if ActivityLog gains soft delete later, or
    • destroy it only if it is imported and unmodified by the user.
  • Track user edits with an imported flag and, optionally, user_modified_at.

Google Health API Client Layer

Follow project rules: do not create app/services/.

Use Google's official generated Ruby REST client for this integration:

gem "google-apis-health_v4", "~> 0.3.0"
gem "googleauth", "~> 1.16"

Notes:

  • omniauth-google-oauth2 is already in the project, but it is for OAuth login/authorization flow, not for calling Google Health API methods.
  • google-apis-health_v4 is the generated Google Health API v4 client from googleapis/google-api-ruby-client.
  • googleauth handles Google authorization objects used by the generated API clients.
  • Avoid the older monolithic google-api-client gem. The current Google pattern is per-service gems named google-apis-<service>_<version>.
  • Direct REST with Net::HTTP should be a fallback only if the generated client is missing a method or the Health API changes faster than the gem is regenerated.

Suggested files:

app/models/health/google/client.rb
app/models/health/google/oauth_client.rb
app/models/health/google/token_refresher.rb
app/models/health/google/data_type_fetcher.rb
app/models/health/google/exercise_importer.rb
app/models/health/google/daily_summary_importer.rb
app/models/health/activity_log_projector.rb
app/models/health/habit_rule_evaluator.rb
app/models/health/sync_runner.rb

Client responsibilities:

  • Wrap Google::Apis::HealthV4 calls behind Lifehub-friendly methods.
  • Refresh access tokens using the refresh token.
  • Handle 401 by refreshing once and retrying.
  • Handle rate limits with retry/backoff in jobs.
  • Normalize Google Health responses into Lifehub records.

Controller And Route Plan

Suggested authenticated routes:

resource :health, only: %i[show], controller: "health" do
  post :sync
end

resources :health_connections, only: %i[index destroy] do
  collection do
    get :google_authorize
    get :google_callback
  end
end

resources :health_sport_mappings, only: %i[index update]
resources :health_habit_rules, only: %i[index create update destroy]

Webhook route outside authenticated user session:

namespace :webhooks do
  post "google_health", to: "google_health#create"
end

Controller responsibilities:

  • HealthController#show: dashboard and sync status.
  • HealthConnectionsController#google_authorize: starts OAuth flow.
  • HealthConnectionsController#google_callback: exchanges code and stores connection.
  • HealthConnectionsController#destroy: disconnects and revokes tokens if possible.
  • HealthSportMappingsController: lets user map provider exercise types to Lifehub sports.
  • HealthHabitRulesController: lets user configure automatic habit completion.
  • Webhooks::GoogleHealthController#create: validates webhook and enqueues sync.

Authorization

Policies should follow the existing (user, record) pattern.

Suggested policies:

  • HealthPolicy for singleton dashboard: signed-in user can view/sync their own health page.
  • Health::ConnectionPolicy
  • Health::SportMappingPolicy
  • Health::HabitRulePolicy

Scopes always filter by user_id: user.id.


User Experience Plan

Health Dashboard

First screen should be the actual Health dashboard, not a marketing page.

Sections:

  • Connection status: connected/disconnected/sync failed.
  • Last synced timestamp.
  • Sync now button.
  • Today summary: steps, active minutes, calories, sleep, resting HR.
  • This week activity: workouts, minutes, distance.
  • Trends: steps, sleep, resting HR, weight.
  • Recent imported activity logs.
  • Setup prompts for sport mappings and habit rules.

Connection Flow

  1. User opens Health page.
  2. Clicks Connect Google Health.
  3. Google OAuth consent requests only selected scopes.
  4. Callback stores connection.
  5. Initial backfill job starts.
  6. User sees Syncing... state and partial data as it arrives.

Sport Mapping UI

Show provider exercise types discovered from sync:

Provider type Lifehub sport Behavior
running Corrida Create activity logs
strength_training Musculação Create activity logs
pickleball No sport Create custom sport / keep unmapped / ignore

Habit Rule UI

Examples of user-facing rules:

  • Complete Exercício físico when any workout is at least 20 minutes.
  • Complete Caminhada 30 min when steps reach 6000 or walking is at least 30 minutes.
  • Complete Musculação when Google Health exercise type is strength_training.

Implementation Phases

Phase 0 — Google Health API Validation Spike

Goal: prove the OAuth and first API call before building the full UI.

Tasks:

  • Create Google Cloud project and OAuth client.
  • Enable Google Health API.
  • Use OAuth Playground or a minimal Rails callback to request dev scopes.
  • Fetch users/me/identity.
  • Fetch one data type, preferably exercise or steps.
  • Confirm exact response IDs, timestamps, pagination, and data type names.

Deliverable:

  • Notes added to this docs folder with real response shape examples, redacted.

Phase 1 — Schema And Models

Tasks:

  • Add migrations for health tables.
  • Add provider fields to activity_logs.
  • Update ActivityLog to allow optional sport only if the UI supports it.
  • Add User associations:
    • has_one :health_connection
    • has_many :health_data_points
    • has_many :health_exercise_sessions
    • has_many :health_daily_summaries
    • has_many :health_sport_mappings
    • has_many :health_habit_rules
  • Add enums, validations, and scopes.

Tests:

  • Model validation tests.
  • Unique provider ID tests.
  • Optional sport activity log tests.

Phase 2 — OAuth Connection

Tasks:

  • Add credentials config for Google Health client ID/secret.
  • Build Health::Google::OauthClient.
  • Add authorize/callback routes.
  • Store encrypted refresh token and scopes.
  • Fetch and store Google Health user identity.
  • Add disconnect/revoke behavior.

Tests:

  • Callback success.
  • Callback failure.
  • Missing credentials handling.
  • Disconnected status.

Phase 3 — Sync Engine

Tasks:

  • Build Health::SyncRunner.
  • Build data type fetchers.
  • Build token refresher.
  • Build importers for exercise sessions and daily summaries.
  • Add Health::SyncJob.
  • Add recurring job in config/recurring.yml.
  • Add manual Sync now action.

Tests:

  • Token refresh path.
  • Idempotent upsert.
  • Retry behavior.
  • Scheduled job only syncs connected users.

Phase 4 — Activity Log Projection

Tasks:

  • Build Health::ActivityLogProjector.
  • Implement sport mapping lookup.
  • Implement provider sport auto-creation.
  • Implement no-sport custom activity fallback.
  • Ensure activity goals refresh through existing callbacks.
  • Add UI for imported activity log labels.

Tests:

  • Existing sport is used when mapped.
  • Custom sport is created when enabled.
  • No-sport imported log works when auto-create disabled.
  • Rerunning sync updates existing log instead of duplicating.
  • Deleted provider session does not destroy user-modified logs.

Phase 5 — Habit Auto-Completion

Tasks:

  • Build Health::HabitRuleEvaluator.
  • Add habit rule UI.
  • Evaluate rules after exercise import and daily summary import.
  • Use habit.complete_on! for existing streak/goal behavior.
  • Decide and implement XP behavior for same-day imports.

Tests:

  • Any workout completes a habit.
  • Specific exercise type completes a habit.
  • Steps threshold completes a habit.
  • Existing completion is not duplicated.
  • Weekly/monthly habit period behavior remains correct.

Phase 6 — Webhooks

Tasks:

  • Add webhook controller.
  • Validate Google Health webhook payloads according to current docs.
  • Register subscriber endpoint in Google Health API.
  • Enqueue lightweight sync job from webhook.
  • Store webhook timestamps and errors.

Tests:

  • Valid webhook enqueues job.
  • Invalid webhook is rejected.
  • Webhook is idempotent.

Phase 7 — Health Dashboard And Analytics

Tasks:

  • Add Health nav entry.
  • Build dashboard cards and charts.
  • Add connection management.
  • Add sport mapping and habit rule settings.
  • Add sync status/error states.
  • Add empty states before connection.

Tests:

  • Dashboard loads for connected/disconnected users.
  • User only sees own health data.
  • Sync errors are visible but not noisy.

Phase 8 — Hardening And Compliance

Tasks:

  • Review OAuth scopes and request minimum necessary access.
  • Add privacy policy text for health data usage.
  • Add data deletion/disconnect behavior.
  • Add admin-free operational visibility through Mission Control Jobs.
  • Run bin/ci.
  • Run Brakeman and bundler audit.

Suggested File Inventory

New Models

app/models/health/connection.rb
app/models/health/data_point.rb
app/models/health/exercise_session.rb
app/models/health/daily_summary.rb
app/models/health/sport_mapping.rb
app/models/health/habit_rule.rb
app/models/health/google/client.rb
app/models/health/google/oauth_client.rb
app/models/health/google/token_refresher.rb
app/models/health/google/data_type_fetcher.rb
app/models/health/google/exercise_importer.rb
app/models/health/google/daily_summary_importer.rb
app/models/health/activity_log_projector.rb
app/models/health/habit_rule_evaluator.rb
app/models/health/sync_runner.rb

New Controllers

app/controllers/health_controller.rb
app/controllers/health_connections_controller.rb
app/controllers/health_sport_mappings_controller.rb
app/controllers/health_habit_rules_controller.rb
app/controllers/webhooks/google_health_controller.rb

New Jobs

app/jobs/health/sync_job.rb
app/jobs/health/backfill_job.rb
app/jobs/health/webhook_sync_job.rb

New Policies

app/policies/health_policy.rb
app/policies/health/connection_policy.rb
app/policies/health/sport_mapping_policy.rb
app/policies/health/habit_rule_policy.rb

New Views

app/views/health/show.html.erb
app/views/health_connections/index.html.erb
app/views/health_sport_mappings/index.html.erb
app/views/health_habit_rules/index.html.erb

Modified Files

app/models/user.rb
app/models/activity_log.rb
app/views/activity_logs/_form.html.erb
app/views/activity_logs/index.html.erb
app/views/sports/index.html.erb
app/views/sports/show.html.erb
config/routes.rb
config/recurring.yml
config/locales/en.yml
config/locales/pt-BR.yml

External Setup: Step By Step

These are the things that must happen outside the code, Master.

1. Create Or Choose A Google Cloud Project

  1. Go to Google Cloud Console: https://console.cloud.google.com
  2. Create a new project, e.g. Lifehub Health.
  3. Select the project in the top project switcher.

2. Enable Google Health API

  1. Go to APIs & Services → Library.
  2. Search for Google Health API.
  3. Enable it for the project.
  4. Confirm the API endpoint/docs point to health.googleapis.com.
  1. Go to APIs & Services → OAuth consent screen.
  2. Choose external or internal depending on the deployment.
  3. Fill in:
    • App name: Lifehub
    • User support email
    • Developer contact email
    • App logo if available
    • App domain
    • Privacy Policy URL
    • Terms of Service URL
  4. Add authorized domain, e.g. lifehub.yourdomain.com.
  5. Keep app in testing mode during development.
  6. Add test users while in testing mode.

4. Add OAuth Scopes

Request the minimum scopes needed for the first release.

Recommended v1 read-only scopes:

https://www.googleapis.com/auth/googlehealth.profile.readonly
https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly
https://www.googleapis.com/auth/googlehealth.sleep.readonly
https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly

Optional later scopes:

https://www.googleapis.com/auth/googlehealth.nutrition.readonly
https://www.googleapis.com/auth/googlehealth.location.readonly

Avoid write scopes in v1.

5. Create OAuth Client Credentials

  1. Go to APIs & Services → Credentials.
  2. Click Create Credentials → OAuth client ID.
  3. Choose Web application.
  4. Name it Lifehub Web.
  5. Add authorized redirect URIs:

Development:

http://localhost:3000/health_connections/google_callback
http://127.0.0.1:3000/health_connections/google_callback
http://127.0.0.1:3004/health_connections/google_callback

Production:

https://yourdomain.com/health_connections/google_callback
  1. Save the generated Client ID and Client Secret.

6. Store Credentials In Rails

Run:

bin/rails credentials:edit

Add:

google_health:
  client_id: "xxx.apps.googleusercontent.com"
  client_secret: "GOCSPX-xxx"
  redirect_uri: "http://127.0.0.1:3004/health_connections/google_callback"

For production, use the production redirect URI in production credentials.

7. Prepare Public URLs For Webhooks

Google Health webhooks require a public HTTPS endpoint.

Development options:

  • Use a tunnel such as Cloudflare Tunnel or ngrok.
  • Example endpoint:
https://your-dev-tunnel.example.com/webhooks/google_health

Production endpoint:

https://yourdomain.com/webhooks/google_health

8. Register Google Health Subscriber / Webhook

After the webhook controller exists:

  1. Use Google Health API subscriber endpoints to create a subscriber for the project.
  2. Set endpoint URI to /webhooks/google_health.
  3. Subscribe only to data types Lifehub syncs.
  4. Store the subscriber name/id in Rails credentials or a database setting.

Expected subscriber operations from Google Health API:

  • create subscriber
  • list subscribers
  • patch subscriber configuration
  • delete subscriber

9. Verify In Testing Mode

  1. Add your Google account as a test user.
  2. Connect Google Health from Lifehub.
  3. Confirm the consent screen shows only the intended scopes.
  4. Confirm callback returns to Lifehub.
  5. Confirm Health::Connection is connected.
  6. Run a manual sync.
  7. Confirm exercise sessions and daily summaries import.
  8. Confirm activity logs are created only once.
  9. Confirm mapped habits complete correctly.

10. Prepare For Production Verification

Before moving beyond test users:

  1. Publish/update Privacy Policy with health data usage.
  2. Publish/update Terms of Service if needed.
  3. Complete Google OAuth app verification if Google requires it for sensitive health scopes.
  4. Confirm domain ownership in Google Search Console if requested.
  5. Confirm production redirect URI exactly matches Google Cloud.
  6. Confirm webhook endpoint is HTTPS and reachable.
  7. Confirm disconnect deletes/revokes access and stops sync jobs.

Security And Privacy Requirements

  • Request minimum scopes.
  • Prefer read-only scopes in v1.
  • Encrypt access and refresh tokens at rest.
  • Do not log tokens, authorization codes, or raw sensitive payloads.
  • Avoid showing detailed health errors in flash messages.
  • Let users disconnect Google Health.
  • On disconnect, stop syncs immediately.
  • Decide whether disconnect deletes imported data or keeps it; default recommendation: ask the user.
  • Store raw payloads only if necessary for debugging/reconciliation.
  • Add a retention policy before importing high-volume granular data.

Testing Strategy

Unit Tests

  • Health::Connection validation and status transitions.
  • Health::Google::TokenRefresher token refresh behavior.
  • Health::ExerciseSession idempotent upsert.
  • Health::ActivityLogProjector mapping behavior.
  • Health::HabitRuleEvaluator rule matching.
  • Health::DailySummary uniqueness and aggregation behavior.

Integration Tests

  • OAuth callback creates connection.
  • Manual sync enqueues job.
  • Webhook enqueues sync job.
  • Disconnect revokes/stops sync.

System Tests

  • User connects Google Health.
  • User sees syncing state.
  • User maps provider exercise type to sport.
  • User configures a habit auto-completion rule.

Job Tests

  • Backfill can be rerun without duplicates.
  • Scheduled sync only runs for connected users.
  • Sync failure stores last_error and keeps connection recoverable.

Operational Checklist

  • Add recurring sync to config/recurring.yml.
  • Monitor jobs in Mission Control Jobs.
  • Add retry/backoff for rate limits.
  • Add admin-safe logs without health payload contents.
  • Add a manual resync action for users.
  • Add a support/debug view showing connection status and last sync time.

Open Decisions

  1. Should imported same-day activity award XP by default? Recommendation: no for v1.
  2. Should disconnect delete imported Health data? Recommendation: ask user with two options: keep imported data or delete imported data.
  3. Should Lifehub auto-create provider sports by default? Recommendation: yes, but only after first sync shows a review screen.
  4. Should unmapped provider activity create an ActivityLog with no sport? Recommendation: yes, to avoid data loss.
  5. Should granular heart-rate samples be stored? Recommendation: no in v1; use daily summaries unless needed for charts.
  6. Should we add Google’s official Ruby client/auth library? Decision: yes. Use google-apis-health_v4 and googleauth, and update AGENTS.md allowed gems before implementation.

  1. External Google Cloud setup and API validation spike.
  2. Health schema and models.
  3. OAuth connect/disconnect.
  4. Manual sync for exercise and steps.
  5. Activity log projection.
  6. Sport mapping UI.
  7. Habit rule evaluation.
  8. Daily summaries and dashboard.
  9. Scheduled sync.
  10. Webhooks.
  11. Privacy/compliance hardening.
  12. Full CI and production rollout.