Google Health API Connection - Implementation Plan
Status: Planning artifact Primary reference:
docs/google-health/project-plan.mdGoal: Turn the Google Health project plan into a practical, file-by-file implementation sequence for Lifehub.
Implementation Principles
- Keep the integration in a first-class
Healthdomain instead of forcing all provider data intoActivityLog. - Use the real Google Health API at
https://health.googleapis.com/v4as the only v1 provider. - Call the REST API directly with
Net::HTTP— thegoogle-apis-health_v4generated client is not published on rubygems, so we use only the realgoogleauthgem for OAuth2 token refresh. - Keep Google login OAuth separate from Google Health authorization.
- Store raw provider payloads for traceability, but make Lifehub screens read normalized records.
- Make every sync idempotent. A failed or repeated job must not duplicate activity logs, habit completions, or XP.
- Keep records user-owned with
belongs_to :user. - Do not create
app/services/. Use namespaced model classes underapp/models/health/.
Release Shape
Build this as three releases instead of one large merge.
- Foundation release: gems, credentials, database tables, models, policies, and a connection screen.
- Sync release: OAuth connection, Google client, manual sync, scheduled sync, and normalized imports.
- Automation release: activity log projection, sport mappings, habit rules, webhooks, and analytics polish.
This lets us ship the connection safely before automatic behavior starts changing a user's activity and habit history.
Phase 0 - Repository Preparation
0.1 Update Allowed Gems
Update AGENTS.md before adding the new gem so the repo rules match the implementation.
Add this under the allowed integration gems:
gem "googleauth", "~> 1.16"
Keep the note that omniauth-google-oauth2 is still used for login OAuth and should not be treated as the Google Health API client.
0.2 Add Gems
Update Gemfile near the existing OmniAuth section:
# Google Health API — OAuth2 token refresh only. All Google Health REST calls
# are made directly via Net::HTTP from Health::Google::Client.
gem "googleauth", "~> 1.16"
Then run:
bundle install
0.3 Verify Connectivity
After bundling, hit the discovery doc from a one-off command to confirm network reachability and the v4 API surface:
require "net/http"
require "uri"
Net::HTTP.get(URI("https://health.googleapis.com/$discovery/rest?version=v4"))
Phase 1 - Configuration And Credentials
1.1 Rails Credentials
Store Google Health configuration in encrypted credentials.
Suggested shape:
google_health:
client_id: "..."
client_secret: "..."
project_id: "..."
webhook_secret: "..."
Use these through a small config object, not scattered credential lookups.
1.2 Config Object
Create:
app/models/health/google/configuration.rb
Responsibilities:
- Read
Rails.application.credentials.google_health. - Expose
client_id,client_secret,project_id,webhook_secret. - Expose redirect URI helper.
- Expose default read scopes.
- Raise a clear error if required production credentials are missing.
Suggested default scopes:
SCOPES = [
"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",
"https://www.googleapis.com/auth/googlehealth.nutrition.readonly",
"https://www.googleapis.com/auth/googlehealth.profile.readonly"
].freeze
Start with readonly scopes. Writing data back to Google Health is not part of v1.
1.3 Token Encryption Check
Before adding token fields, verify whether Active Record encryption is already configured.
If configured, use encrypted attributes on Health::Connection:
encrypts :access_token
encrypts :refresh_token
If not configured, add Rails encryption keys before shipping this feature. Do not store OAuth tokens as plain text.
Phase 2 - Database Schema
Create migrations in this order.
2.1 health_connections
Purpose: stores provider authorization, account identity, and sync state.
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
t.text :refresh_token
t.datetime :access_token_expires_at
t.json :scopes, default: []
t.json :settings, default: {}
t.datetime :last_synced_at
t.datetime :last_incremental_sync_at
t.datetime :last_backfill_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
If encrypted attributes require different column names in this app, adapt before generating the migration.
2.2 health_data_points
Purpose: normalized health samples, intervals, and daily summaries.
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]
add_index :health_data_points, :deleted_at
2.3 health_exercise_sessions
Purpose: normalized exercise sessions that can create or update activity logs.
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]
add_index :health_exercise_sessions, :deleted_at
2.4 health_sport_mappings
Purpose: maps provider exercise types to Lifehub sports.
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 :external_exercise_type, null: false
t.string :activity_type
t.boolean :auto_create_activity_logs, null: false, default: true
t.boolean :auto_create_sport, null: false, default: false
t.json :settings, default: {}
t.timestamps
end
add_index :health_sport_mappings, [:user_id, :provider, :external_exercise_type], unique: true, name: "idx_health_sport_mappings_unique_provider_type"
Important behavior:
sport_idcan be null.- If mapped to a sport, activity logs use that sport.
- If no sport is mapped, imported logs may still be created with
sport_id: nilwhen the user allows unmapped imports. - If
auto_create_sportis true, the importer creates a provider sport once and saves it to the mapping.
2.5 health_habit_rules
Purpose: defines when synced health/activity data completes habits automatically.
create_table :health_habit_rules do |t|
t.references :user, null: false, foreign_key: true
t.references :habit, null: false, foreign_key: true
t.references :sport, foreign_key: true
t.string :provider, null: false, default: "google_health"
t.string :rule_type, null: false
t.string :external_exercise_type
t.string :data_type
t.decimal :threshold_value, precision: 15, scale: 4
t.string :threshold_unit
t.boolean :enabled, null: false, default: true
t.json :settings, default: {}
t.timestamps
end
add_index :health_habit_rules, [:user_id, :habit_id]
add_index :health_habit_rules, [:user_id, :enabled]
Suggested rule_type values:
exercise_session- complete when a matching exercise session exists.sport_activity- complete when an imported activity log matches a Lifehub sport.daily_metric_threshold- complete when a daily metric reaches a threshold, such as steps >= 8000.duration_threshold- complete when total activity duration reaches a threshold.
2.6 Extend activity_logs
Purpose: distinguish manual logs from imported logs and make projections idempotent.
add_column :activity_logs, :source, :string, null: false, default: "manual"
add_column :activity_logs, :source_provider, :string
add_column :activity_logs, :source_external_id, :string
add_column :activity_logs, :external_activity_type, :string
add_column :activity_logs, :started_at, :datetime
add_column :activity_logs, :ended_at, :datetime
add_column :activity_logs, :imported_at, :datetime
add_column :activity_logs, :provider_metadata, :json, default: {}
add_index :activity_logs, [:user_id, :source_provider, :source_external_id], unique: true, name: "idx_activity_logs_import_id"
add_index :activity_logs, [:user_id, :source, :date]
Model implication:
- Change
belongs_to :sporttobelongs_to :sport, optional: true. - Add a validation that manual activity logs still require a sport if the existing UI depends on it.
- Imported activity logs may have no
sport_idso Google Health custom/unmapped activities are supported.
Phase 3 - Models And Associations
3.1 User Associations
Add to User:
has_many :health_connections, dependent: :destroy
has_many :health_data_points, dependent: :destroy
has_many :health_exercise_sessions, dependent: :destroy
has_many :health_sport_mappings, dependent: :destroy
has_many :health_habit_rules, dependent: :destroy
3.2 New Models
Create:
app/models/health/connection.rb
app/models/health/data_point.rb
app/models/health/exercise_session.rb
app/models/health/sport_mapping.rb
app/models/health/habit_rule.rb
Model responsibilities:
Health::Connection: token state, provider status, sync settings.Health::DataPoint: metric normalization and scope helpers.Health::ExerciseSession: exercise normalization and projection status.Health::SportMapping: provider exercise type to Lifehub sport behavior.Health::HabitRule: validates automatic habit completion rules.
3.3 Enums
Use string enums where state is fixed.
Suggested enums:
# Health::Connection
enum :provider, { google_health: "google_health" }, default: :google_health
enum :status, {
pending: "pending",
connected: "connected",
sync_failed: "sync_failed",
disconnected: "disconnected",
revoked: "revoked"
}, default: :pending
# Health::DataPoint
enum :record_kind, {
sample: "sample",
interval: "interval",
daily: "daily",
summary: "summary"
}
# Health::HabitRule
enum :rule_type, {
exercise_session: "exercise_session",
sport_activity: "sport_activity",
daily_metric_threshold: "daily_metric_threshold",
duration_threshold: "duration_threshold"
}
3.4 ActivityLog Source Enum
Add an enum or constant-backed validation:
enum :source, {
manual: "manual",
google_health: "google_health"
}, default: :manual
If Rails enum naming conflicts with source, use constants and inclusion validation instead.
Phase 4 - Authorization And Routes
4.1 Policies
Create:
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
Policy pattern:
- Singleton health dashboard: signed-in user can view.
- Records: user can manage records where
record.user_id == user.id. - Scopes:
scope.where(user_id: user.id).
4.2 Routes
Add 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]
namespace :webhooks do
post "google_health", to: "google_health#create"
end
Keep controllers flat unless namespacing is already established for a specific boundary. Webhooks can live under Webhooks:: because they are externally called endpoints.
4.3 Controllers
Create:
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
Controller responsibilities:
HealthController#show: dashboard with connection state, last sync, summary metrics, recent imported sessions.HealthController#sync: enqueueHealth::SyncJobfor current user's connection.HealthConnectionsController#google_authorize: redirect to Google consent.HealthConnectionsController#google_callback: exchange code, store tokens, enqueue initial sync.HealthConnectionsController#destroy: revoke or mark disconnected and stop future syncs.HealthSportMappingsController#index/update: let users map provider exercise types.HealthHabitRulesController: CRUD for auto-completion rules.Webhooks::GoogleHealthController#create: validate webhook request, enqueue webhook sync.
Phase 5 - Google OAuth Connection
5.1 OAuth Client
Create:
app/models/health/google/oauth_client.rb
Responsibilities:
- Build authorization URL with readonly Google Health scopes.
- Include a signed
statevalue tied to the current user. - Request offline access so a refresh token is returned.
- Exchange authorization code for access and refresh tokens.
- Fetch Google Health user identity/profile after connection.
Important behavior:
- Do not reuse login-only OmniAuth callbacks.
- Do not add Health scopes to normal sign-in.
- Store granted scopes on
Health::Connection#scopes. - If Google does not return a new refresh token on reconnect, keep the existing refresh token unless the user intentionally disconnects.
5.2 State Verification
Use Rails message verifier for OAuth state.
State payload:
{
user_id: current_user.id,
purpose: "google_health_connection",
nonce: SecureRandom.hex(16),
issued_at: Time.current.to_i
}
Reject callback if:
- state is missing or invalid.
- state is expired.
user_iddoes not matchcurrent_user.id.- Google returns an error.
5.3 Token Refresh
Create:
app/models/health/google/token_refresher.rb
Responsibilities:
- Refresh expired access tokens with
googleauth/Signet. - Persist new access token and expiration.
- Mark connection
revokedif refresh fails because consent was revoked. - Mark connection
sync_failedfor transient failures.
Phase 6 - Google Health API Client
6.1 Client Wrapper
Create:
app/models/health/google/client.rb
Responsibilities:
- Instantiate the generated Health API service.
- Attach authorization from
Health::Connection. - Expose Lifehub-friendly methods:
identityprofilesettingsdata_points(data_type:, start_time:, end_time:, page_token: nil)daily_rollup(data_type:, start_date:, end_date:)exercise_sessions(start_time:, end_time:)subscribe_webhook!
- Refresh and retry once after
401. - Let jobs handle retry/backoff for rate limits and transient failures.
6.2 Importers
Create:
app/models/health/google/data_point_importer.rb
app/models/health/google/exercise_importer.rb
app/models/health/google/daily_summary_importer.rb
Responsibilities:
- Fetch provider data through
Health::Google::Client. - Normalize payloads into model attributes.
- Upsert by
provider + external_id. - Soft-delete records when Google reports deletions or reconcile indicates records no longer exist.
- Store
raw_payloadfor auditing.
6.3 Data Type Registry
Create:
app/models/health/google/data_type_registry.rb
Responsibilities:
- Centralize supported Google Health data types.
- Define Lifehub unit normalization.
- Define whether a data type imports as raw data points, daily summaries, exercise sessions, or all of those.
Start with:
exercisestepsdistanceactive-minutesactive-zone-minutestotal-caloriessleepheart-ratedaily-resting-heart-rateheart-rate-variabilitydaily-heart-rate-variabilityoxygen-saturationvo2-maxweightbody-fatheighthydration-log
Phase 7 - Sync Jobs
7.1 Jobs
Create:
app/jobs/health/sync_job.rb
app/jobs/health/backfill_job.rb
app/jobs/health/webhook_sync_job.rb
app/jobs/health/daily_summary_job.rb
app/jobs/health/project_activity_logs_job.rb
app/jobs/health/evaluate_habit_rules_job.rb
Job responsibilities:
Health::SyncJob: incremental sync for one connection.Health::BackfillJob: historical import in bounded chunks.Health::WebhookSyncJob: short incremental sync after Google webhook.Health::DailySummaryJob: roll up daily metrics after raw imports.Health::ProjectActivityLogsJob: create/updateActivityLogrecords from exercise sessions.Health::EvaluateHabitRulesJob: complete habits based on synced sessions and daily metrics.
7.2 Sync Runner
Create:
app/models/health/sync_runner.rb
Responsibilities:
- Coordinate importers for one connection.
- Compute sync window.
- Persist
last_synced_atonly after successful import. - Enqueue projection and habit evaluation after imported records are committed.
- Store errors on
Health::Connection#last_error.
Window rules:
- First manual sync: last 30 days.
- Initial backfill: chunk backward by month until configured limit.
- Scheduled sync: from
last_incremental_sync_at - 1.hourto now, to tolerate late provider updates. - Webhook sync: from
last_webhook_at - 1.hourto now.
7.3 Recurring Jobs
Update config/recurring.yml:
health_sync:
class: Health::SyncJob
schedule: every 2 hours
If Solid Queue recurring jobs require arguments, create a fan-out job:
app/jobs/health/sync_connected_users_job.rb
That job finds connected Google Health connections and enqueues Health::SyncJob per connection.
Phase 8 - Activity Log Projection
8.1 Projector
Create:
app/models/health/activity_log_projector.rb
Responsibilities:
- Convert
Health::ExerciseSessioninto anActivityLog. - Upsert by
user_id + source_provider + source_external_id. - Link
health_exercise_session.activity_log_id. - Update existing imported activity logs when provider data changes.
- Soft-delete or archive activity logs if the source session is deleted, based on product decision.
8.2 Projection Rules
For each exercise session:
- Find or create
Health::SportMappingbyexternal_exercise_type. - If mapping has
sport_id, use that sport. - If mapping has
auto_create_sport, create aSportnamed from the provider exercise type and save it on the mapping. - If there is no sport and unmapped imports are enabled, create an activity log with
sport_id: nilandactivity_type: "custom". - If unmapped imports are disabled, keep only the
Health::ExerciseSessionand mark it as not projected.
8.3 ActivityLog Attributes
Suggested projection:
{
user: session.user,
sport: mapped_sport,
source: "google_health",
source_provider: session.provider,
source_external_id: session.external_id,
external_activity_type: session.exercise_type,
activity_type: mapped_sport&.sport_type || "custom",
date: session.date,
started_at: session.started_at,
ended_at: session.ended_at,
duration_minutes: session.duration_minutes,
notes: generated_notes,
sport_data: session.metrics,
provider_metadata: {
source_name: session.source_name,
source_device: session.source_device,
distance_meters: session.distance_meters,
calories_kcal: session.calories_kcal,
average_heart_rate: session.average_heart_rate,
max_heart_rate: session.max_heart_rate
},
imported_at: Time.current
}
8.4 Manual Edits To Imported Logs
Add a product rule before implementation:
- Imported logs can be edited for Lifehub-only fields such as notes, sport mapping, and intensity.
- Provider-owned fields such as start/end time, duration, and metrics should be updated by sync.
- If a user changes the sport on an imported log, save or suggest a
Health::SportMappingupdate.
Phase 9 - Habit Automation
9.1 Evaluator
Create:
app/models/health/habit_rule_evaluator.rb
Responsibilities:
- Evaluate enabled
Health::HabitRulerecords after sync. - Complete habits when evidence satisfies the rule.
- Use
Habit#complete_on!so streaks and goal callbacks continue to work. - Keep completion idempotent through the existing unique index on
habit_completions. - Add notes that identify the completion source.
9.2 Rule Examples
Running habit completed by imported runs:
rule_type: exercise_session
external_exercise_type: running
threshold_value: 1
Gym habit completed by a mapped sport:
rule_type: sport_activity
sport_id: <gym sport id>
threshold_value: 1
Steps habit completed by daily metric:
rule_type: daily_metric_threshold
data_type: steps
threshold_value: 8000
threshold_unit: count
Cardio habit completed by duration:
rule_type: duration_threshold
external_exercise_type: cycling
threshold_value: 30
threshold_unit: minutes
9.3 XP Decision
For v1, do not award XP for automatic imports.
Reason: existing XP methods are called from manual controllers and are not idempotent for provider replay. Automatic XP can be added later with a unique source key such as:
google_health:<habit_id>:<date>:<rule_id>
google_health_activity:<activity_log_id>
Document this clearly in the Health UI so users understand that imports help tracking and goals, while manual actions remain the intentional gamification path in v1.
Phase 10 - Webhooks
10.1 Google Subscriber Setup
Create a class:
app/models/health/google/subscriber.rb
Responsibilities:
- Create or update Google Health subscriber configuration.
- Store subscriber identifiers in
Health::Connection#settings. - Disable subscriber on disconnect if Google supports it for the configured resource.
10.2 Webhook Controller
Webhooks::GoogleHealthController#create should:
- Skip Devise authentication if Google cannot send user session cookies.
- Validate request authenticity using the Google Health webhook verification mechanism and
webhook_secret. - Parse only the minimal event metadata.
- Find the matching
Health::Connection. - Update
last_webhook_at. - Enqueue
Health::WebhookSyncJob. - Return a fast
200 OK.
If the exact Google Health webhook signing scheme differs from this expectation, implement the documented verification flow from the API reference before enabling production webhooks.
Phase 11 - Views And UX
11.1 Navigation
Add Health to the authenticated app navigation near activity, habits, or analytics.
11.2 Health Dashboard
Create:
app/views/health/show.html.erb
Show:
- Connection status.
- Connect/disconnect actions.
- Last sync time.
- Manual sync button.
- Recent imported exercise sessions.
- Recent activity logs created by Google Health.
- Basic all-time and recent metrics: steps, distance, active minutes, sleep, resting heart rate, weight.
- Sync errors with a retry action.
11.3 Sport Mapping UI
Create:
app/views/health_sport_mappings/index.html.erb
Show provider exercise types discovered from imports and allow:
- Map to existing sport.
- Leave unmapped but still create custom activity logs.
- Auto-create a Lifehub sport.
- Disable automatic activity log creation for that exercise type.
11.4 Habit Rule UI
Create:
app/views/health_habit_rules/index.html.erb
Allow the user to create rules from existing habits:
- Habit selector.
- Rule type selector.
- Sport or provider exercise type selector.
- Data type selector for metric-based rules.
- Threshold value and unit.
- Enabled toggle.
Phase 12 - Analytics
12.1 Health Summary Calculator
Create:
app/models/health/summary_calculator.rb
Responsibilities:
- Summarize health data by period.
- Provide dashboard cards and charts.
- Keep queries scoped to
user.health_data_pointsanduser.health_exercise_sessions.
Initial metrics:
- Total workouts.
- Total exercise minutes.
- Total distance.
- Steps by day/week/month.
- Sleep duration by day/week/month.
- Resting heart rate trend.
- Weight trend.
12.2 Activity And Goal Integration
No new goal tracking mode is required for v1 if activity logs and habit completions are created correctly.
Existing callbacks should refresh:
activity_log_countactivity_log_durationhabit_streakhabit_completions
Add direct Health goal modes later only if users need goals based on metrics that do not project to activity logs or habits.
Phase 13 - Tests
Use Minitest and fixtures.
13.1 Model Tests
Add tests for:
Health::Connectionvalidations and enums.Health::DataPointidempotency fields.Health::ExerciseSessionvalidates required provider fields.Health::SportMappingsupports null sport mappings.Health::HabitRulevalidates rule-specific fields.ActivityLogaccepts nil sport for imported logs but preserves manual behavior.
13.2 Importer Tests
Add tests for:
- Exercise import creates
Health::ExerciseSessiononce. - Re-import updates the existing session.
- Data point import upserts by provider/external ID.
- Token refresh is called on expired token.
- Revoked token marks connection revoked.
13.3 Projector Tests
Add tests for:
- Mapped provider exercise creates activity log with sport.
- Unmapped provider exercise creates activity log without sport when enabled.
- Auto-create sport creates one sport and reuses it.
- Reprojection updates the same activity log.
- Deleted provider session does not duplicate or orphan logs.
13.4 Habit Rule Tests
Add tests for:
- Exercise session completes habit on the session date.
- Daily steps threshold completes habit once.
- Duration threshold sums same-day sessions correctly.
- Disabled rules do nothing.
- Re-running evaluator does not create duplicate completions.
13.5 Controller Tests
Add tests for:
- Health dashboard requires authentication.
- Manual sync enqueues job.
- OAuth callback rejects invalid state.
- OAuth callback creates or updates connection.
- Mapping updates are user-scoped.
- Habit rules are user-scoped.
- Webhook validates signature and enqueues sync.
13.6 Job Tests
Add tests for:
- Sync job runs importer and updates connection timestamps.
- Sync job records errors and does not advance sync timestamp on failure.
- Backfill job chunks time windows.
- Webhook sync job uses a short incremental window.
Phase 14 - External Google Setup
The external setup checklist lives in docs/google-health/project-plan.md. During implementation, complete it in this order:
- Create or select a Google Cloud project.
- Enable Google Health API.
- Configure OAuth consent screen.
- Add readonly Google Health scopes.
- Create OAuth 2.0 Web Application credentials.
- Add redirect URI:
https://your-domain.com/health_connections/google_callback
For local development, add:
http://localhost:3000/health_connections/google_callback
Use the actual local port when testing.
- Add production webhook URL:
https://your-domain.com/webhooks/google_health
- Store credentials in Rails encrypted credentials.
- Complete Google verification steps if the selected scopes require app verification.
- Test with a Google account that has Health data available.
Phase 15 - Rollout And Safety
15.1 Feature Flag
Add a simple configuration guard before exposing the UI broadly:
Rails.configuration.x.google_health.enabled
Default to false in production until credentials, OAuth consent, and webhook setup are complete.
15.2 Initial Rollout
Recommended order:
- Enable connect/disconnect only.
- Enable manual sync for one test user.
- Enable normalized imports without creating activity logs.
- Enable activity log projection.
- Enable habit rules.
- Enable recurring sync.
- Enable webhooks.
15.3 Observability
Track at minimum:
- Number of connected users.
- Last successful sync per connection.
- Sync failures by error class.
- Imported exercise sessions count.
- Activity logs created by Google Health.
- Habit completions created by rules.
- Webhook deliveries received.
Keep errors visible in the Health dashboard and logs.
Implementation Checklist
- Update
AGENTS.mdallowed gems. - Add
google-apis-health_v4andgoogleauthtoGemfile. - Bundle and verify generated Health API service class.
- Add Google Health encrypted credentials.
- Add Health configuration model.
- Add database migrations.
- Add Health models and user associations.
- Update
ActivityLogto support imported source fields and optional sport for imports. - Add policies.
- Add routes and controllers.
- Add OAuth authorization and callback flow.
- Add token refresher.
- Add Google Health API client wrapper.
- Add data type registry.
- Add data point and exercise importers.
- Add sync jobs and recurring fan-out job.
- Add activity log projector.
- Add sport mapping UI.
- Add habit rule evaluator.
- Add habit rule UI.
- Add Health dashboard and navigation.
- Add webhook subscriber and webhook controller.
- Add tests for models, importers, projectors, rules, controllers, and jobs.
- Run
bin/rails testor targeted tests during implementation. - Run
bin/cibefore pushing.
Key Open Decisions Before Coding
- How far should initial backfill go? Recommended v1 default: 12 months, with monthly chunks.
- Should imported activity logs be editable? Recommended: yes for Lifehub-owned fields, no for provider-owned metrics.
- Should automatic imports award XP? Recommended v1: no. Add idempotent XP later.
- Should deleted provider sessions delete activity logs? Recommended: soft-delete or mark source deleted first, then decide based on user-facing behavior.
- Should unmapped provider exercises create activity logs by default? Recommended: yes, with
sport_id: nilandactivity_type: custom, because the user explicitly wants custom/unregistered provider activities supported.
Definition Of Done
The Google Health implementation is complete when:
- A user can connect and disconnect Google Health from Lifehub.
- Tokens are encrypted and refresh correctly.
- Manual sync imports normalized health data without duplicates.
- Recurring sync keeps connected users updated.
- Google Health exercise sessions create or update Lifehub activity logs idempotently.
- Imported activities can be mapped to existing sports, auto-created sports, or no sport.
- Habit rules can auto-complete habits from imported sessions or daily metrics.
- Existing activity and habit goals refresh from imported records.
- Webhooks enqueue incremental syncs safely.
- Tests cover the sync, projection, and rule automation paths.
- External Google setup is documented and completed for the target environment.