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}/dataPointswhere{dataType}is a kebab-case slug (exercise,steps,sleep,heart-rate, etc.). - No generated Ruby client:
google-apis-health_v4is not published on rubygems. The implementation calls the REST API directly withNet::HTTPand uses only the realgoogleauthgem 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 aHealth::Google::SubscriberManagerregister/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:
- Automatic syncs: scheduled jobs plus Google Health webhook-triggered incremental syncs.
- Automatic activity logs: synced exercise sessions create or update
ActivityLogrecords, with support for matched existing sports, auto-created provider sports, or logs without a Lifehub sport if needed. - 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
ActivityLogrecords 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 underapp/models/health/.
Current Lifehub Context
Lifehub already has:
User has_many :sportsUser has_many :activity_logsUser has_many :habitsActivityLog belongs_to :userandbelongs_to :sportActivityLogfields:activity_type,date,duration_minutes,intensity,notes,sport_data,sport_id,user_id- Activity goals auto-refresh after
ActivityLogsave/destroy. - Habit goals auto-refresh after
HabitCompletionsave/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.
settingsshould include user preferences:auto_create_activity_logs: trueauto_create_sports: trueaward_xp_for_imports: falsesync_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_kindvalues:sample,interval,daily,summary.data_typeexamples: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_idlinks the synced session to its Lifehub projection.sport_idis 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ísicoif any exercise session has duration >= 20 minutes. - Complete
Caminhada 30 minif steps >= 6,000 or walking session duration >= 30 minutes. - Complete
Beber 2L de Águaif hydration volume >= 2 liters, if we later sync hydration. - Complete a specific gym habit if exercise type is
strength_trainingand 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:
- Upsert
Health::ExerciseSessionby[provider, external_id]. - Skip projection if the user disabled automatic activity logs.
- Find a
Health::SportMappingfor the provider exercise type. - If mapping has
sport_id, use that sport. - If no mapping and
auto_create_sportsis 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.
- If no mapping and auto-create is disabled:
- Create
ActivityLogwith no sport,activity_type: "custom", and provider details insport_data.
- Create
- Upsert
ActivityLogby[source_provider, external_id]. - Set
activity_log_idon theHealth::ExerciseSession. - 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_sessionexercise_type_durationsport_durationsteps_totaldistance_totalactive_minutes_totalcalories_totalhydration_total
Completion Flow
After syncing a session or daily summary:
- Find active rules for the user.
- Evaluate rules for the affected date.
- If a rule passes, call
habit.complete_on!(date, value:, notes:). - Add notes such as
Completed from Google Health: 42 min running. - Recalculate streaks through the existing habit completion flow.
- Keep idempotency through the unique
[habit_id, date]index onhabit_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_attohealth_exercise_sessionsandhealth_habit_rulesoutcome records later if XP becomes product-critical.
Sync Design
Sync Modes
- 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.
- Scheduled sync
- Runs every 1-3 hours through Solid Queue recurring jobs.
- Syncs from
last_synced_at - overlap_windowto now. - Use a 24-hour overlap to catch provider corrections.
- 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.
- Manual sync
- A button in the Health page:
Sync now. - Rate-limited by connection, e.g. no more than once every 5 minutes.
- A button in the Health page:
Data Types For V1
Start with these Google Health API data types:
exercisestepsdistanceactive-minutesactive-zone-minutestotal-caloriessleepheart-ratedaily-resting-heart-rateheart-rate-variabilityvo2-maxweightbody-fatoxygen-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
importedflag 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-oauth2is already in the project, but it is for OAuth login/authorization flow, not for calling Google Health API methods.google-apis-health_v4is the generated Google Health API v4 client fromgoogleapis/google-api-ruby-client.googleauthhandles Google authorization objects used by the generated API clients.- Avoid the older monolithic
google-api-clientgem. The current Google pattern is per-service gems namedgoogle-apis-<service>_<version>. - Direct REST with
Net::HTTPshould 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::HealthV4calls behind Lifehub-friendly methods. - Refresh access tokens using the refresh token.
- Handle
401by 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:
HealthPolicyfor singleton dashboard: signed-in user can view/sync their own health page.Health::ConnectionPolicyHealth::SportMappingPolicyHealth::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 nowbutton.- 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
- User opens Health page.
- Clicks
Connect Google Health. - Google OAuth consent requests only selected scopes.
- Callback stores connection.
- Initial backfill job starts.
- 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ísicowhen any workout is at least20minutes. - Complete
Caminhada 30 minwhen steps reach6000or walking is at least30minutes. - Complete
Musculaçãowhen Google Health exercise type isstrength_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
exerciseorsteps. - 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
ActivityLogto allow optional sport only if the UI supports it. - Add
Userassociations:has_one :health_connectionhas_many :health_data_pointshas_many :health_exercise_sessionshas_many :health_daily_summarieshas_many :health_sport_mappingshas_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 nowaction.
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
- Go to Google Cloud Console: https://console.cloud.google.com
- Create a new project, e.g.
Lifehub Health. - Select the project in the top project switcher.
2. Enable Google Health API
- Go to APIs & Services → Library.
- Search for Google Health API.
- Enable it for the project.
- Confirm the API endpoint/docs point to
health.googleapis.com.
3. Configure OAuth Consent Screen
- Go to APIs & Services → OAuth consent screen.
- Choose external or internal depending on the deployment.
- Fill in:
- App name:
Lifehub - User support email
- Developer contact email
- App logo if available
- App domain
- Privacy Policy URL
- Terms of Service URL
- App name:
- Add authorized domain, e.g.
lifehub.yourdomain.com. - Keep app in testing mode during development.
- 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
- Go to APIs & Services → Credentials.
- Click Create Credentials → OAuth client ID.
- Choose Web application.
- Name it
Lifehub Web. - 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
- 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:
- Use Google Health API subscriber endpoints to create a subscriber for the project.
- Set endpoint URI to
/webhooks/google_health. - Subscribe only to data types Lifehub syncs.
- 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
- Add your Google account as a test user.
- Connect Google Health from Lifehub.
- Confirm the consent screen shows only the intended scopes.
- Confirm callback returns to Lifehub.
- Confirm
Health::Connectionis connected. - Run a manual sync.
- Confirm exercise sessions and daily summaries import.
- Confirm activity logs are created only once.
- Confirm mapped habits complete correctly.
10. Prepare For Production Verification
Before moving beyond test users:
- Publish/update Privacy Policy with health data usage.
- Publish/update Terms of Service if needed.
- Complete Google OAuth app verification if Google requires it for sensitive health scopes.
- Confirm domain ownership in Google Search Console if requested.
- Confirm production redirect URI exactly matches Google Cloud.
- Confirm webhook endpoint is HTTPS and reachable.
- 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::Connectionvalidation and status transitions.Health::Google::TokenRefreshertoken refresh behavior.Health::ExerciseSessionidempotent upsert.Health::ActivityLogProjectormapping behavior.Health::HabitRuleEvaluatorrule matching.Health::DailySummaryuniqueness 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_errorand 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
- Should imported same-day activity award XP by default? Recommendation: no for v1.
- Should disconnect delete imported Health data? Recommendation: ask user with two options: keep imported data or delete imported data.
- Should Lifehub auto-create provider sports by default? Recommendation: yes, but only after first sync shows a review screen.
- Should unmapped provider activity create an
ActivityLogwith no sport? Recommendation: yes, to avoid data loss. - Should granular heart-rate samples be stored? Recommendation: no in v1; use daily summaries unless needed for charts.
- Should we add Google’s official Ruby client/auth library? Decision: yes. Use
google-apis-health_v4andgoogleauth, and updateAGENTS.mdallowed gems before implementation.
Recommended Build Order
- External Google Cloud setup and API validation spike.
- Health schema and models.
- OAuth connect/disconnect.
- Manual sync for
exerciseandsteps. - Activity log projection.
- Sport mapping UI.
- Habit rule evaluation.
- Daily summaries and dashboard.
- Scheduled sync.
- Webhooks.
- Privacy/compliance hardening.
- Full CI and production rollout.