Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Google Fit

Google Fit has a REST API (the Fitness API) accessible from web apps — unlike HealthKit, it works directly in Bolt.new with no native iOS companion required. Enable the Fitness API in Google Cloud Console, implement OAuth 2.0 to let users authorize access, and fetch step counts, heart rate, and activity data as JSON. The OAuth callback URL requires a deployed domain — test on Netlify or Bolt Cloud after the initial setup.

What you'll learn

  • How to enable the Google Fitness API in Google Cloud Console and configure OAuth 2.0 credentials
  • How to implement the Google OAuth 2.0 Authorization Code flow with a deployed callback URL
  • How to fetch step counts, heart rate, and activity data from Google Fit's REST API
  • How to parse Google Fit's data source and dataset format into chart-friendly arrays
  • How to build a fitness dashboard with Recharts displaying weekly trends
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate19 min read35 minutesOtherApril 2026RapidDev Engineering Team
TL;DR

Google Fit has a REST API (the Fitness API) accessible from web apps — unlike HealthKit, it works directly in Bolt.new with no native iOS companion required. Enable the Fitness API in Google Cloud Console, implement OAuth 2.0 to let users authorize access, and fetch step counts, heart rate, and activity data as JSON. The OAuth callback URL requires a deployed domain — test on Netlify or Bolt Cloud after the initial setup.

Build a Fitness Dashboard in Bolt.new with the Google Fit REST API

Google Fit is one of the few fitness platforms that provides a full REST API accessible from web applications — a direct contrast to Apple's HealthKit, which requires a native iOS app. The Google Fitness API runs over HTTPS, returns JSON, and integrates naturally with Bolt.new's WebContainer. For developers building web-based fitness dashboards, activity trackers, or wellness apps, Google Fit is the practical path for Android users and cross-platform fitness data.

The Fitness API uses a data model centered around data sources and datasets. A data source represents a stream of health measurements from a specific app or device (e.g., 'derived:com.google.step_count.delta:com.google.android.gms:estimated_steps'). A dataset is a query result for a specific data source over a time range, returning buckets of aggregated data. This structure is more complex than typical REST APIs — the tutorial covers how to parse it into simple arrays for Recharts.

The OAuth flow is standard Google OAuth 2.0 Authorization Code. Users click a 'Connect Google Fit' button, authorize your app on Google's consent screen, and are redirected back to your app with an authorization code. Your server exchanges the code for access and refresh tokens. The refresh token lets you fetch new access tokens silently without re-prompting the user. The critical constraint: Google's OAuth redirect URI must be a registered, stable domain. You cannot use Bolt's WebContainer preview URL for this — deploy to Netlify first, register the domain, then test the full OAuth flow.

Integration method

Bolt Chat + API Route

Google Fit's REST API (Fitness API) communicates entirely over HTTPS and is fully compatible with Bolt.new's WebContainer. Users authorize your app via OAuth 2.0 consent, and your Next.js API routes fetch their fitness data using access tokens. The OAuth callback URL must be a deployed domain — the WebContainer's dynamic preview URL cannot be registered in Google Cloud Console. Build the data-fetching and chart logic in Bolt preview, then test OAuth on the deployed Netlify URL.

Prerequisites

  • A Google account with Google Fit data (install the Google Fit app on Android or connect a fitness tracker to populate data for testing)
  • A Google Cloud project at console.cloud.google.com with the Fitness API enabled
  • OAuth 2.0 credentials (Client ID and Client Secret) from Google Cloud Console → APIs & Services → Credentials
  • A Bolt.new account with a new Next.js project open
  • A Netlify account for deploying and registering the OAuth redirect URI

Step-by-step guide

1

Enable the Fitness API and create OAuth credentials in Google Cloud Console

Setting up Google Cloud is the most involved prerequisite for this integration. Start at console.cloud.google.com and create a new project (or use an existing one). In the left navigation, go to APIs & Services → Library. Search for 'Fitness API' and enable it. This makes the API endpoints available for your project. Next, create OAuth credentials: go to APIs & Services → Credentials → Create Credentials → OAuth client ID. Before creating the credentials, you may need to configure the OAuth consent screen — go to APIs & Services → OAuth consent screen first. Choose 'External' for user type (unless you are a Google Workspace org), add your app name, support email, and developer contact email. Under scopes, click 'Add or Remove Scopes' and add: `https://www.googleapis.com/auth/fitness.activity.read` (step counts, active minutes) and `https://www.googleapis.com/auth/fitness.body.read` (heart rate, weight). Add test users (your own Gmail) to the test users list. Back in Credentials → Create Credentials → OAuth client ID: choose 'Web application' as the application type. Under Authorized redirect URIs, add both `http://localhost:3000/api/fit/callback` for local testing and your future Netlify URL `https://your-app.netlify.app/api/fit/callback` — add a placeholder now and update it after deployment. Download the JSON or copy the Client ID and Client Secret. Note: while your app is in 'Testing' mode (not published), only users listed in the test users list can authorize it. This is fine for development and personal use. For public apps, you need to go through Google's OAuth app verification process.

Bolt.new Prompt

Set up a Next.js project for Google Fit integration. Create a .env.local file with: GOOGLE_CLIENT_ID=your_client_id, GOOGLE_CLIENT_SECRET=your_client_secret, GOOGLE_REDIRECT_URI=http://localhost:3000/api/fit/callback, NEXTAUTH_SECRET=a_random_32_char_string. Create a lib/google-fit.ts file that exports constants for the Fitness API base URL and scopes. Install no extra packages for now — we will use native fetch for API calls.

Paste this in Bolt.new chat

lib/google-fit.ts
1// lib/google-fit.ts
2export const GOOGLE_OAUTH_BASE = 'https://accounts.google.com/o/oauth2';
3export const GOOGLE_FIT_BASE = 'https://www.googleapis.com/fitness/v1';
4
5export const FIT_SCOPES = [
6 'https://www.googleapis.com/auth/fitness.activity.read',
7 'https://www.googleapis.com/auth/fitness.body.read',
8 'https://www.googleapis.com/auth/fitness.sleep.read',
9].join(' ');
10
11export interface FitDataPoint {
12 date: string;
13 value: number;
14}
15
16export async function refreshAccessToken(refreshToken: string): Promise<string> {
17 const response = await fetch('https://oauth2.googleapis.com/token', {
18 method: 'POST',
19 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
20 body: new URLSearchParams({
21 client_id: process.env.GOOGLE_CLIENT_ID!,
22 client_secret: process.env.GOOGLE_CLIENT_SECRET!,
23 refresh_token: refreshToken,
24 grant_type: 'refresh_token',
25 }),
26 });
27
28 if (!response.ok) {
29 throw new Error('Failed to refresh Google access token');
30 }
31
32 const data = await response.json();
33 return data.access_token;
34}
35
36export function nanoToMillis(nanos: number): number {
37 return Math.floor(nanos / 1_000_000);
38}
39
40export function millisToDateString(ms: number): string {
41 return new Date(ms).toISOString().split('T')[0];
42}

Pro tip: While developing, add yourself as a test user in the Google Cloud OAuth consent screen. Apps in 'Testing' mode are limited to 100 test users. You do not need to submit for Google's app review process for personal projects or internal tools — only for apps used by the general public.

Expected result: Fitness API enabled in Google Cloud Console, OAuth credentials created with Client ID and Client Secret, and a configured lib/google-fit.ts in your Bolt project.

2

Implement the OAuth 2.0 authorization flow

The OAuth flow requires three API routes: a login initiator that builds the Google consent URL, a callback handler that exchanges the authorization code for tokens, and a utility that refreshes access tokens using the long-lived refresh token. The login route constructs a URL pointing to `https://accounts.google.com/o/oauth2/v2/auth` with required parameters: `client_id`, `redirect_uri`, `response_type=code`, `scope` (space-separated fitness scopes), `access_type=offline` (required to receive a refresh token), and `prompt=consent` (forces Google to always return a refresh token, even if the user previously authorized). Generate and store a `state` parameter in a cookie to prevent CSRF attacks. The callback route receives the authorization code after user consent, validates the state, and calls Google's token endpoint to exchange the code for tokens. The response contains both an access token (expires in 1 hour) and a refresh token (long-lived). Store the refresh token in an HTTP-only cookie — it lets you get fresh access tokens without re-prompting the user. The critical constraint: Google only issues a refresh token the first time a user authorizes your app (or when `prompt=consent` is set). If you lose the refresh token, the user must re-authorize. Always store it securely on the first authorization. Remember: the OAuth callback URL must be a registered domain. Deploy to Netlify first, then update GOOGLE_REDIRECT_URI and add the production URL to Google Cloud Console's authorized redirect URIs.

Bolt.new Prompt

Implement Google OAuth for Google Fit in my Next.js app. Create three API routes: (1) /api/fit/login — generates a state string, stores it in a cookie, and redirects to Google's OAuth URL with fitness.activity.read fitness.body.read fitness.sleep.read scopes, access_type=offline, prompt=consent. (2) /api/fit/callback — validates state cookie, exchanges the code for tokens at https://oauth2.googleapis.com/token, stores the refresh_token in an HTTP-only cookie named google_refresh_token (expires 30 days), and redirects to /dashboard. (3) /api/fit/me — reads the refresh_token cookie, exchanges it for an access token, calls https://www.googleapis.com/oauth2/v2/userinfo to get the user's name and picture, and returns them. Use process.env.GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI.

Paste this in Bolt.new chat

app/api/fit/callback/route.ts
1// app/api/fit/callback/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4export async function GET(request: NextRequest) {
5 const { searchParams } = request.nextUrl;
6 const code = searchParams.get('code');
7 const state = searchParams.get('state');
8 const error = searchParams.get('error');
9
10 if (error === 'access_denied') {
11 return NextResponse.redirect(new URL('/?error=fit_denied', request.url));
12 }
13
14 const storedState = request.cookies.get('google_oauth_state')?.value;
15 if (!state || state !== storedState) {
16 return NextResponse.redirect(new URL('/?error=state_mismatch', request.url));
17 }
18
19 const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
20 method: 'POST',
21 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
22 body: new URLSearchParams({
23 code: code!,
24 client_id: process.env.GOOGLE_CLIENT_ID!,
25 client_secret: process.env.GOOGLE_CLIENT_SECRET!,
26 redirect_uri: process.env.GOOGLE_REDIRECT_URI!,
27 grant_type: 'authorization_code',
28 }),
29 });
30
31 if (!tokenResponse.ok) {
32 return NextResponse.redirect(new URL('/?error=token_failed', request.url));
33 }
34
35 const tokens = await tokenResponse.json();
36 const response = NextResponse.redirect(new URL('/dashboard', request.url));
37
38 if (tokens.refresh_token) {
39 response.cookies.set('google_refresh_token', tokens.refresh_token, {
40 httpOnly: true,
41 secure: process.env.NODE_ENV === 'production',
42 sameSite: 'lax',
43 maxAge: 30 * 24 * 60 * 60, // 30 days
44 path: '/',
45 });
46 }
47
48 response.cookies.delete('google_oauth_state');
49 return response;
50}

Pro tip: Always include prompt=consent in the OAuth authorization URL. Without it, Google may skip the consent screen for users who previously authorized your app and will not return a refresh token if they already have one. The prompt=consent parameter guarantees a fresh refresh token is issued on every authorization.

Expected result: OAuth flow that redirects users to Google's consent screen and returns a refresh token stored in an HTTP-only cookie after authorization.

3

Fetch step count and activity data from the Fitness API

The Google Fitness API uses a data aggregation model that differs from typical REST APIs. Instead of `GET /steps?date=2024-03-15`, you POST a query body describing the data type, time range, and bucket size. The endpoint is `https://www.googleapis.com/fitness/v1/users/me/dataset:aggregate`. The request body specifies the aggregateBy data type (`com.google.step_count.delta` for steps), the bucketByTime duration (86400000 milliseconds = 1 day for daily buckets), and the startTimeMillis / endTimeMillis range as milliseconds since epoch. The response returns an array of buckets, each with a dataset array, each with a point array containing the actual values. This nested structure is intentional — a single time bucket may have multiple data points from different sources (phone pedometer, watch, third-party app). For a simple step count chart, sum all values in the point array for each bucket and map the `startTimeMillis` to a date string. For heart rate, use the data type `com.google.heart_rate.bpm` with the `AVERAGE` aggregation type. This returns average BPM per day rather than individual readings. Build an API route that handles token refresh automatically: read the refresh token from the cookie, get a fresh access token, make the Fitness API call, and return structured data. If the refresh token is missing or invalid, return a 401 that triggers the frontend to redirect to the login flow.

Bolt.new Prompt

Create Google Fit data API routes. First: /api/fit/steps - reads the google_refresh_token cookie, uses refreshAccessToken from lib/google-fit.ts to get a fresh access token, POSTs to https://www.googleapis.com/fitness/v1/users/me/dataset:aggregate requesting step_count.delta data bucketed by day for the last 30 days. Parse the nested response into an array of {date: string, steps: number} objects sorted by date. Second: /api/fit/heart-rate - same pattern but for heart_rate.bpm data with AVERAGE aggregation. Return {date: string, bpm: number}[] . Handle 401 responses (invalid token) by returning status 401.

Paste this in Bolt.new chat

app/api/fit/steps/route.ts
1// app/api/fit/steps/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { refreshAccessToken, GOOGLE_FIT_BASE, millisToDateString } from '@/lib/google-fit';
4
5export async function GET(request: NextRequest) {
6 const refreshToken = request.cookies.get('google_refresh_token')?.value;
7 if (!refreshToken) {
8 return NextResponse.json({ error: 'Not authenticated', redirect: '/api/fit/login' }, { status: 401 });
9 }
10
11 let accessToken: string;
12 try {
13 accessToken = await refreshAccessToken(refreshToken);
14 } catch {
15 return NextResponse.json({ error: 'Token refresh failed', redirect: '/api/fit/login' }, { status: 401 });
16 }
17
18 const endTimeMs = Date.now();
19 const startTimeMs = endTimeMs - 30 * 24 * 60 * 60 * 1000; // 30 days ago
20
21 const body = {
22 aggregateBy: [{ dataTypeName: 'com.google.step_count.delta' }],
23 bucketByTime: { durationMillis: 86400000 }, // 1 day
24 startTimeMillis: startTimeMs,
25 endTimeMillis: endTimeMs,
26 };
27
28 const response = await fetch(`${GOOGLE_FIT_BASE}/users/me/dataset:aggregate`, {
29 method: 'POST',
30 headers: {
31 'Authorization': `Bearer ${accessToken}`,
32 'Content-Type': 'application/json',
33 },
34 body: JSON.stringify(body),
35 });
36
37 if (!response.ok) {
38 return NextResponse.json({ error: `Fitness API error: ${response.status}` }, { status: response.status });
39 }
40
41 const data = await response.json();
42
43 // Parse nested bucket → dataset → point structure
44 const stepsByDay = data.bucket
45 .map((bucket: any) => {
46 const date = millisToDateString(parseInt(bucket.startTimeMillis));
47 const steps = bucket.dataset[0]?.point?.reduce(
48 (sum: number, point: any) => sum + (point.value[0]?.intVal ?? 0),
49 0
50 ) ?? 0;
51 return { date, steps };
52 })
53 .filter((d: any) => d.steps > 0);
54
55 return NextResponse.json({ data: stepsByDay });
56}

Pro tip: Google Fit returns buckets for every day in the range, including days with zero steps (where the user did not use any Google Fit-connected device). Filter out zero-step days with .filter(d => d.steps > 0) to avoid flat regions in your charts that suggest the user was completely inactive when they just were not tracked.

Expected result: API routes that return structured step and heart rate data as simple date/value arrays, parsed from the Fitness API's nested bucket-dataset-point format.

4

Build the fitness dashboard with Recharts

With data flowing from the API routes, build the fitness dashboard. Use Recharts for visualizations — it is pure JavaScript, compatible with Bolt's WebContainer, and integrates cleanly with React and TypeScript. The dashboard has three sections: summary stat cards at the top (average daily steps, average resting heart rate, total active days this month), a daily steps bar chart for the last 30 days with a 10,000-step goal reference line, and a heart rate trend line chart. For the daily steps chart, a bar chart communicates daily totals better than a line chart — the discrete daily nature of steps is clearer as bars. Color the bars conditionally: green for days meeting the 10,000-step goal, amber for 7,000-9,999, red for below 7,000. This requires a custom bar cell renderer in Recharts. Add a date range selector (Last 7 Days, Last 30 Days) that filters the data client-side. Avoid re-fetching on range changes — load 30 days of data once and slice it based on the selected range. Protect the dashboard route by checking for the Google refresh token cookie. If the cookie is absent, redirect to the connect page that shows the 'Connect Google Fit' button linking to `/api/fit/login`.

Bolt.new Prompt

Build a Google Fit dashboard page at app/dashboard/page.tsx. On mount, fetch data from /api/fit/steps and /api/fit/heart-rate in parallel. Show a loading state while fetching. If either returns 401, show a 'Connect Google Fit' button that links to /api/fit/login. Once data loads: (1) Show summary cards: avg daily steps (30-day), days goal met (>10000 steps), avg resting heart rate. (2) A Recharts BarChart of daily steps with bars colored green/amber/red based on goal achievement, with a ReferenceLine at 10000 labeled 'Daily Goal'. (3) A Recharts LineChart of daily heart rate with a smooth curve. Add a 7-day/30-day toggle that filters displayed data. Use a clean fitness app design: white cards, green (#22c55e) primary color, dark gray text.

Paste this in Bolt.new chat

app/components/StepsChart.tsx
1// app/components/StepsChart.tsx
2'use client';
3import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ReferenceLine, Cell, ResponsiveContainer } from 'recharts';
4
5interface StepsData { date: string; steps: number; }
6
7function getBarColor(steps: number): string {
8 if (steps >= 10000) return '#22c55e'; // green — goal met
9 if (steps >= 7000) return '#f59e0b'; // amber — close
10 return '#ef4444'; // red — below 7k
11}
12
13export function StepsChart({ data }: { data: StepsData[] }) {
14 const formatDate = (dateStr: string) => {
15 const d = new Date(dateStr + 'T12:00:00');
16 return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
17 };
18
19 return (
20 <div className="bg-white rounded-xl p-5 shadow-sm border border-gray-100">
21 <h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-1">Daily Steps</h3>
22 <p className="text-xs text-gray-400 mb-4">Green = goal met (10,000+)</p>
23 <ResponsiveContainer width="100%" height={220}>
24 <BarChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
25 <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" vertical={false} />
26 <XAxis
27 dataKey="date"
28 tickFormatter={formatDate}
29 tick={{ fontSize: 10, fill: '#9ca3af' }}
30 interval={Math.floor(data.length / 6)}
31 />
32 <YAxis tick={{ fontSize: 10, fill: '#9ca3af' }} tickFormatter={v => `${(v/1000).toFixed(0)}k`} />
33 <Tooltip
34 formatter={(value: number) => [value.toLocaleString(), 'Steps']}
35 labelFormatter={formatDate}
36 />
37 <ReferenceLine y={10000} stroke="#22c55e" strokeDasharray="4 4" label={{ value: 'Goal', fontSize: 10, fill: '#22c55e' }} />
38 <Bar dataKey="steps" radius={[3, 3, 0, 0]}>
39 {data.map((entry, index) => (
40 <Cell key={index} fill={getBarColor(entry.steps)} />
41 ))}
42 </Bar>
43 </BarChart>
44 </ResponsiveContainer>
45 </div>
46 );
47}

Pro tip: Google Fit step data for yesterday and today may be incomplete depending on when the user's device last synced. Consider adding a note in the UI: 'Data may be delayed up to 24 hours based on device sync'. This is a common source of user confusion — they expect to see today's steps in real time.

Expected result: A complete fitness dashboard with color-coded step count bars, heart rate trend chart, summary stat cards, and date range filtering.

5

Deploy to Netlify and configure production OAuth

Google Fit's OAuth callback works only with registered redirect URIs — Bolt.new's WebContainer preview URL changes between sessions and cannot be registered in Google Cloud Console. Deploy to Netlify to get a stable domain for the OAuth callback. In Bolt.new, click Deploy and connect to Netlify. After deployment, copy your Netlify URL (e.g., `your-app.netlify.app`). In Netlify's dashboard → Site Configuration → Environment Variables, add all required variables: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI` (set to `https://your-app.netlify.app/api/fit/callback`), and `NEXTAUTH_SECRET`. Back in Google Cloud Console → APIs & Services → Credentials → your OAuth client → Edit: add `https://your-app.netlify.app/api/fit/callback` to the Authorized Redirect URIs list. Save. Trigger a Netlify redeploy for the new environment variables to apply. Test the full OAuth flow on the deployed site: click 'Connect Google Fit', authorize the app, and confirm you land on the dashboard with your fitness data. If you see a redirect_uri_mismatch error, the URI in your Google Cloud Console does not match `GOOGLE_REDIRECT_URI` exactly — compare them character by character. During development in Bolt's WebContainer, you can test the data-fetching API routes by manually hardcoding a token, but the full OAuth redirect flow requires the deployed Netlify URL.

Bolt.new Prompt

Add a connect page and logout functionality to my Google Fit app. Create app/connect/page.tsx with a hero section explaining the benefits of connecting Google Fit, a 'Connect Google Fit' button linking to /api/fit/login, and a list of what data will be accessed (steps, heart rate, sleep). Add an API route at /api/fit/logout that clears the google_refresh_token cookie and redirects to /connect. Add a logout button to the dashboard header. Also create netlify.toml with the Next.js build configuration.

Paste this in Bolt.new chat

netlify.toml
1# netlify.toml
2[build]
3 command = "npm run build"
4 publish = ".next"
5
6[[plugins]]
7 package = "@netlify/plugin-nextjs"
8
9[build.environment]
10 NODE_VERSION = "20"
11
12# Security headers
13[[headers]]
14 for = "/*"
15 [headers.values]
16 X-Frame-Options = "SAMEORIGIN"
17 X-Content-Type-Options = "nosniff"
18 Strict-Transport-Security = "max-age=31536000; includeSubDomains"

Pro tip: After deploying, go to Google Cloud Console → OAuth consent screen and add your production domain to the Authorized domains list. Without this, users may see a 'This app is not verified by Google' warning screen during OAuth. Adding the domain removes this warning for your domain.

Expected result: A fully deployed fitness dashboard on Netlify with working Google Fit OAuth and production fitness data loading for authorized users.

Common use cases

Personal Fitness Activity Dashboard

A dashboard showing the authenticated user's weekly step count, daily active minutes, and calories burned over the last 30 days. Uses line and bar charts to visualize trends, with summary cards showing weekly totals and progress toward daily goals.

Bolt.new Prompt

Build a Google Fit fitness dashboard in Next.js. Implement OAuth: create /api/fit/login that redirects to Google OAuth with fitness.activity.read and fitness.body.read scopes. Create /api/fit/callback that exchanges the code for tokens and stores the refresh_token in an HTTP-only cookie. Create /api/fit/steps that fetches 30 days of aggregated step counts using the Fitness API's dataAggregation endpoint (bucketByTime by day). Build a dashboard page with a bar chart of daily steps (Recharts), a line chart of weekly active minutes, and summary stat cards. Use process.env.GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.

Copy this prompt to try it in Bolt.new

Team Wellness Challenge Leaderboard

A company wellness challenge app where employees connect their Google Fit accounts and their weekly step data is synced to Supabase. The leaderboard shows team rankings by weekly steps with progress bars and an office competition dashboard.

Bolt.new Prompt

Build a wellness leaderboard in Next.js with Supabase. After Google Fit OAuth, fetch the user's last 7 days of step data from the Fitness API and store the daily totals in a Supabase health_metrics table (columns: user_id, date, steps). Create a leaderboard page that queries the past 7 days of steps grouped by user, sorted by total steps descending. Show each user's name, avatar, weekly step total, and a progress bar toward a 70,000-step goal. Add a personal trend chart showing the logged-in user's last 30 days of daily steps.

Copy this prompt to try it in Bolt.new

Sleep and Recovery Tracker

Combine Google Fit's sleep session data with activity data to show recovery insights. Display sleep duration per night, resting heart rate trends, and activity intensity distribution — giving athletes and health-conscious users a consolidated view of their recovery status.

Bolt.new Prompt

Build a sleep and recovery tracker in Next.js using Google Fit. After OAuth with fitness.sleep.read and fitness.body.read scopes, fetch the last 14 days of sleep sessions from the Fitness API sessions endpoint. Display a timeline showing each night's sleep start/end time and total duration in hours. Fetch resting heart rate data and show it as a 14-day line chart. Add a recovery score card calculated as: if sleep > 7h and resting HR < 65 = 'Well rested', if sleep 6-7h = 'Moderate', if sleep < 6h = 'Under-recovered'.

Copy this prompt to try it in Bolt.new

Troubleshooting

Google OAuth returns 'redirect_uri_mismatch' error during authorization

Cause: The redirect URI sent in the OAuth request does not exactly match one of the Authorized Redirect URIs registered in Google Cloud Console. Even a trailing slash difference or http vs https mismatch causes this error.

Solution: In Google Cloud Console → APIs & Services → Credentials → your OAuth client, verify the exact URI in the Authorized Redirect URIs list matches your GOOGLE_REDIRECT_URI environment variable exactly. Both must use https for production. After updating either, wait 5 minutes for Google's changes to propagate.

typescript
1// Verify these match exactly:
2// Google Cloud Console URI: https://your-app.netlify.app/api/fit/callback
3// GOOGLE_REDIRECT_URI env var: https://your-app.netlify.app/api/fit/callback
4// Both must be https in production. No trailing slash difference allowed.

The Fitness API returns empty buckets — no steps or heart rate data in the response

Cause: Two common reasons: (1) the user has not connected any Google Fit data source (no phone app or wearable syncing data), or (2) the requested time range has no data because the user's device did not sync during that period.

Solution: Ask the user to open the Google Fit app on their phone and ensure it is recording steps. In the API response, log the raw bucket data to see what data sources are available. Try expanding the time range to 90 days. Note that the Fitness API does not backfill historical data — if a device was not connected to Google Fit during a period, there is no data for those dates.

typescript
1// Add debug logging to see raw bucket data:
2console.log('Fitness API buckets:', JSON.stringify(data.bucket.slice(0, 3), null, 2));
3// This shows you the nested structure and confirms whether values are present

Dashboard shows 'Not authenticated' and redirects to login even though the user just authorized

Cause: The HTTP-only cookie storing the refresh token is not being set correctly. This happens when the Bolt preview URL uses a different domain than expected, or when the cookie's sameSite setting blocks it.

Solution: Check that the callback route sets the cookie with the correct path ('/') and that secure is false during local development (the preview uses HTTP, not HTTPS). After deploying to Netlify (HTTPS), set secure: true. Log the token response body to confirm Google is returning a refresh_token field.

typescript
1// In the callback route, log the token response during debugging:
2const tokens = await tokenResponse.json();
3console.log('Token response keys:', Object.keys(tokens));
4// If refresh_token is missing, add prompt=consent to the login URL
5// Google only returns refresh_token on the first authorization or with prompt=consent

Google Fit API returns steps for old days but nothing for yesterday or today

Cause: Google Fit data synchronizes from the device to Google's servers on a schedule — typically when the device charges or connects to WiFi. Recent steps may not yet be reflected in the API even if they show in the Google Fit app on the device.

Solution: This is expected behavior and not a bug. Add a UI note explaining that data may be delayed up to a few hours. You can also provide a 'Refresh' button that re-fetches from the API, though the data will only update once Google Fit has synced from the device.

Best practices

  • Request only the minimum OAuth scopes needed — users are more likely to authorize apps that request fewer permissions, and fitness.activity.read covers most common use cases without requesting body or sleep data unless you use them
  • Always store the Google refresh token in an HTTP-only cookie, not in localStorage — HTTP-only cookies are not accessible to JavaScript and cannot be stolen via XSS attacks
  • Cache Fitness API responses for at least 5 minutes — the data updates infrequently (device sync frequency) and re-fetching on every page load wastes quota and slows the dashboard
  • Handle the case where no Google Fit data sources are connected — show a helpful empty state explaining that the user needs to connect a device or the Google Fit app before data will appear
  • Always include prompt=consent in the OAuth authorization URL to guarantee you receive a refresh token on every authorization
  • Deploy to Netlify and test OAuth on the deployed URL before considering the integration complete — OAuth flows cannot be tested in Bolt.new's WebContainer preview
  • Parse Google Fit's nanosecond timestamps carefully — the API uses both millisecond (startTimeMillis) and nanosecond (startTimeNanos) timestamp fields. Use the millisecond fields to avoid overflow issues in JavaScript
  • Add a 'Disconnect Google Fit' button that clears the refresh token cookie — users should always be able to revoke your app's access without going to Google's account settings

Alternatives

Frequently asked questions

Can I test Google Fit API calls in Bolt.new's WebContainer preview?

You can test the data-fetching API routes in the preview — outbound HTTP calls to the Fitness API work in the WebContainer. However, the OAuth callback flow requires a registered redirect URI, which must be a stable domain. Bolt's WebContainer preview uses a dynamic URL that changes between sessions and cannot be registered in Google Cloud Console. Test the OAuth flow on your deployed Netlify URL.

What is the difference between Google Fit and HealthKit for a Bolt.new integration?

Google Fit has a full REST API (the Fitness API) accessible from any web app over HTTPS — you can integrate it directly in Bolt.new. HealthKit is iOS-only with no web API; accessing it from a Bolt app requires a native iOS companion app or the Apple Health XML export approach. For web-first fitness apps, Google Fit is significantly simpler to integrate.

Does Google Fit API work without a wearable device?

Yes. The Google Fit app on any Android phone automatically tracks steps using the phone's accelerometer, records activity sessions, and syncs to Google's servers. You can also connect third-party apps like MyFitnessPal, Strava, and Withings to Google Fit. The API returns all synced data regardless of source. However, if a user has no Android phone, no Wear OS watch, and no connected apps, their Google Fit account will have no data.

Do I need to submit my app for Google verification to use the Fitness API?

Not for personal projects or internal tools. Apps in 'Testing' mode can be used by up to 100 test users (listed in your OAuth consent screen settings) without Google verification. For public apps used by the general public, Google requires app verification — a review process that checks your privacy policy, security practices, and use of sensitive scopes. Fitness API scopes are considered sensitive and require verification for public apps.

How often does Google Fit API data update?

Google Fit syncs from devices to Google's servers when the device charges, connects to WiFi, or the Fit app is opened. This means recent data (last few hours) may not appear in the API even though it shows in the phone app. The delay is typically 1-6 hours for active users. Build your dashboard to communicate this expectation and consider showing the last-sync timestamp if available.

How do I handle expired Google access tokens in my Bolt app?

Google access tokens expire after 1 hour. Use the refresh token stored in the HTTP-only cookie to get a new access token via the refreshAccessToken function in lib/google-fit.ts. Call this function in each API route before making Fitness API requests. If the refresh token is also invalid (revoked or expired), return a 401 error and redirect the user to re-authorize via /api/fit/login.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.