# How to Integrate Lovable with Garmin Connect

- Tool: Lovable
- Difficulty: Intermediate
- Time required: 70 minutes
- Last updated: March 2026

## TL;DR

Integrating Garmin Connect with Lovable uses Edge Functions to handle Garmin's OAuth 1.0a authentication and proxy requests for activities, daily summaries, body composition, and sleep data. Store your Garmin API credentials in Cloud Secrets, implement HMAC-SHA1 request signing per Garmin's OAuth 1.0a requirements, and build athletic performance dashboards. Access requires applying to the Garmin Health API program. Setup takes 60-75 minutes.

## Why integrate Garmin Connect with Lovable?

Garmin is the dominant brand among serious athletes and outdoor enthusiasts, with GPS watches used by marathon runners, triathletes, cyclists, and hikers. Garmin Connect's Health API provides the richest sports performance dataset of any wearable API: GPS activity files with second-by-second heart rate, power, and pace data, daily training load and recovery metrics, VO2 max estimates and fitness age scores, HRV status and stress tracking, sleep stage analysis, and body battery energy levels.

This depth of data is what differentiates Garmin from Fitbit for sports applications. A Fitbit integration tells you how many steps someone took. A Garmin integration tells you their running efficiency, training stress score, time in each heart rate zone, and whether they're at risk of overtraining. For coaches, performance analysts, sports training platforms, and serious athletes tracking their own progress, Garmin's data is unmatched among consumer wearables.

Garmin's API differs from most fitness APIs in one important way: it's push-based rather than pull-based. Instead of your app requesting data from Garmin, Garmin sends data to your webhook endpoint when activities are recorded or daily summaries are generated. This architecture means you need a webhook receiver Edge Function running at a publicly accessible URL where Garmin can push updates. Your app then reads data from your own Supabase database rather than querying Garmin directly on demand.

## Before you start

- A Lovable project with Cloud enabled and a publicly accessible URL (required for Garmin webhooks)
- A Garmin developer account — register at developer.garmin.com
- Garmin Health API access (requires applying at developer.garmin.com/gc-developer-program/overview/ — may take several days for approval)
- OAuth 1.0a consumer key and secret from Garmin after approval
- Understanding that Garmin uses a push model — Garmin sends data to you, not the reverse

## Step-by-step guide

### 1. Apply for Garmin Health API access

Garmin's Health API is not self-serve — it requires applying to their developer program. Go to developer.garmin.com/gc-developer-program/overview/ and click 'Get Started'. You'll complete a form describing your application, intended use case (coaching platform, research, personal health app, etc.), and your organization. Garmin reviews applications and typically responds within 3-5 business days.

Once approved, Garmin provides you with OAuth 1.0a credentials: a Consumer Key and Consumer Secret. These are different from OAuth 2.0 credentials — there's no client_id/client_secret separation, and the authentication flow uses HMAC-SHA1 signing rather than Bearer tokens.

Garmin offers two API tiers: the standard Health API for end-user applications where users connect their Garmin accounts, and the Activity and Wellness API for personal developer use or research. For a coaching platform or multi-user app, you need the standard API with OAuth 1.0a user authorization. For accessing only your own Garmin data, direct API access is available.

During the application, you must provide a webhook callback URL where Garmin will push data. Since you're building on Lovable, use your Edge Function's URL: https://your-project.supabase.co/functions/v1/garmin-webhook. Note that the Lovable preview URL won't work for Garmin webhooks — you need the deployed Supabase Edge Function URL from the Cloud panel.

**Expected result:** Your Garmin developer application is submitted. You have a Consumer Key and Consumer Secret once approved. The Edge Function webhook URL is registered with Garmin.

### 2. Store Garmin credentials in Cloud Secrets

Open your Lovable project, click '+', select 'Cloud', and expand Secrets. Add GARMIN_CONSUMER_KEY with your OAuth 1.0a Consumer Key and GARMIN_CONSUMER_SECRET with your Consumer Secret. Also add GARMIN_API_BASE with 'https://healthapi.garmin.com/wellness-api/rest' for the Health API base URL.

Garmin's OAuth 1.0a authentication requires these credentials for every request signature. Unlike OAuth 2.0 where you exchange them once for a Bearer token, OAuth 1.0a requires signing each API request with both the Consumer Secret and the user's OAuth Token Secret. Store user tokens in a Supabase garmin_tokens table: user_id, oauth_token, oauth_token_secret, garmin_user_id.

Garmin's push model means most data arrives via webhooks rather than direct API pulls. However, for fetching historical data (backfilling past activities for a new connection) and fetching specific data types on demand, you'll still make outbound API requests. These use the user's OAuth token alongside your Consumer Key.

The webhook receiver doesn't require the consumer credentials for verifying incoming data — Garmin sends data without signature verification in the initial implementation, though more recent versions support signature headers. Validate incoming webhook data by checking for expected fields rather than cryptographic signature verification.

**Expected result:** GARMIN_CONSUMER_KEY, GARMIN_CONSUMER_SECRET, and GARMIN_API_BASE appear in Cloud Secrets. The garmin_tokens Supabase table is created.

### 3. Create the Garmin webhook receiver Edge Function

Garmin's push model requires a webhook receiver — an Edge Function that listens for POST requests from Garmin containing activity summaries, daily summaries, heart rate data, body composition updates, and sleep records. Garmin calls your registered webhook URL whenever new data is available for any connected user.

The webhook payloads are JSON arrays containing multiple data records. An activity summary includes: summaryId, userId, activityType, startTimeInSeconds, durationInSeconds, distanceInMeters, avgHeartRateInBeatsPerMinute, maxHeartRateInBeatsPerMinute, totalKilocalories, trainingEffectLabel, and more depending on device capabilities.

For daily summaries, Garmin sends: totalSteps, dailyMovementGoal, floorsClimbed, moderateIntensityDurationInSeconds, vigorousIntensityDurationInSeconds, wellnessStartTimeInSeconds, wellnessEndTimeInSeconds, and body battery data when available.

The Edge Function should: validate the incoming payload has expected fields, map Garmin's user_id to your app's user_id (using a mapping table), upsert the data into your Supabase tables (activities, daily_summaries, sleep_summaries), and return a 200 OK response quickly. Process data asynchronously if the payload is large — Garmin may retry if your endpoint doesn't respond within a few seconds.

```
// supabase/functions/garmin-webhook/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type" };

serve(async (req) => {
  if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });

  const supabase = createClient(Deno.env.get("SUPABASE_URL") ?? "", Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "");

  try {
    const body = await req.json();

    // Map Garmin user IDs to app user IDs
    const getUserId = async (garminUserId: string): Promise<string | null> => {
      const { data } = await supabase.from("garmin_user_mapping").select("user_id").eq("garmin_user_id", garminUserId).single();
      return data?.user_id ?? null;
    };

    if (body.activitySummaries) {
      for (const a of body.activitySummaries) {
        const userId = await getUserId(a.userId);
        if (!userId) continue;
        await supabase.from("activities").upsert({
          summary_id: a.summaryId,
          user_id: userId,
          activity_type: a.activityType,
          start_time: new Date(a.startTimeInSeconds * 1000).toISOString(),
          duration_seconds: a.durationInSeconds,
          distance_meters: a.distanceInMeters ?? 0,
          avg_hr: a.avgHeartRateInBeatsPerMinute ?? null,
          max_hr: a.maxHeartRateInBeatsPerMinute ?? null,
          calories: a.totalKilocalories ?? null,
          training_effect: a.trainingEffectLabel ?? "",
        }, { onConflict: "summary_id" });
      }
    }

    if (body.dailySummaries) {
      for (const d of body.dailySummaries) {
        const userId = await getUserId(d.userId);
        if (!userId) continue;
        await supabase.from("daily_summaries").upsert({
          summary_id: d.summaryId,
          user_id: userId,
          date: new Date(d.calendarDate).toISOString().split("T")[0],
          steps: d.totalSteps ?? 0,
          step_goal: d.dailyMovementGoal ?? 0,
          floors: d.floorsClimbed ?? 0,
          moderate_intensity_minutes: Math.round((d.moderateIntensityDurationInSeconds ?? 0) / 60),
          vigorous_intensity_minutes: Math.round((d.vigorousIntensityDurationInSeconds ?? 0) / 60),
        }, { onConflict: "summary_id" });
      }
    }

    return new Response(JSON.stringify({ received: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
  } catch (err) {
    console.error("Garmin webhook error:", err.message);
    return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: corsHeaders });
  }
});
```

**Expected result:** The garmin-webhook Edge Function is deployed at a public URL. When registered with Garmin, activity and daily summary data appears in your Supabase tables whenever connected users record activities.

### 4. Build the athlete performance dashboard

With data arriving via webhooks and stored in Supabase, build the performance dashboard. Because all data comes from Supabase (not direct Garmin API calls), the dashboard loads fast and works independently of Garmin's API availability.

For coaches managing multiple athletes, the main view should show all athletes with their last activity date, current VO2 max (stored from webhook data), weekly training load, and recovery status (if available from body battery or HRV data). A quick visual indicator (green/yellow/red) shows who is ready to train and who may need rest.

For individual athlete detail, show a 12-week training load trend, recent activities with type, distance, and key metrics, heart rate zone distribution for the current training block, and sleep quality trend if sleep data is being received.

For activity-level detail, show the key metrics for each activity: duration, distance, average and max heart rate, average pace, calories, training effect label (base, aerobic, anaerobic, etc.), and training load score. Build a workout type distribution chart showing how time was split between run, bike, swim, and other activities.

For RapidDev users building coaching platforms with complex multi-athlete views and automated training prescription features, RapidDev's team can help architect the data pipeline and dashboard.

**Expected result:** The performance dashboard shows athlete cards with recent activity data from Supabase. Clicking an athlete shows their training volume chart and activity history.

## Best practices

- Register your Edge Function URL with Garmin as the webhook endpoint during development — Garmin sends data to this URL, so it must be the deployed Edge Function URL, not a Lovable preview URL.
- Create a garmin_user_mapping table that links Garmin user IDs to your Supabase user IDs immediately when users authorize your app — webhook data arrives with Garmin user IDs only.
- Process large webhook batches asynchronously: acknowledge the webhook with 200 immediately, then process the data using a background queue to prevent Garmin from timing out and retrying.
- Garmin's timestamps use Unix epoch seconds, not milliseconds — always multiply by 1000 when converting to JavaScript Date objects or ISO strings.
- Cache activity aggregations (weekly volume, monthly totals) in separate Supabase tables rather than computing them from raw activity data on every dashboard load — aggregated data is much faster to query.
- Store raw Garmin webhook payloads in a raw_webhooks table for 30 days before processing — this gives you a replay buffer if your processing logic has bugs and data needs to be re-processed.
- Apply RLS policies to your activities and daily_summaries tables so coaches can only see data for athletes who have granted them explicit access — athlete health data requires strict privacy controls.

## Use cases

### Athlete performance tracking dashboard

A coaching platform automatically receives Garmin activity data when athletes complete workouts. The webhook receiver Edge Function processes incoming activity summaries and stores training load, heart rate zones, and GPS distance data in Supabase. The coach dashboard shows all athletes' weekly training loads, VO2 max trends, and training stress scores in one view.

Prompt example:

```
Build an athlete performance dashboard. Receive Garmin activity data via webhook Edge Function and store in a Supabase activities table with: athlete_id, activity_type, date, distance, duration, avg_heart_rate, max_heart_rate, training_effect, vo2max, training_load. Display for each athlete: weekly training volume (distance + time), 4-week training load trend chart, current VO2 max estimate, and recovery status. Use data from Supabase, not direct Garmin API calls.
```

### Running pace and heart rate zone analysis

A running coach tool analyzes the distribution of training intensity across heart rate zones from Garmin activities. When a run is received via webhook, the Edge Function stores zone distribution data (time in each HR zone). The analysis page shows zone distribution per workout and a rolling 4-week distribution helping coaches ensure athletes are following polarized training principles.

Prompt example:

```
Build a heart rate zone analyzer for runners. From Garmin webhook data, store each activity's time in 5 heart rate zones (zone1_minutes through zone5_minutes) in the activities table. Create an analysis page showing: time in zone distribution for the last 10 runs as a stacked bar chart, average zone distribution over the past 4 weeks as a pie chart, and a table of recent runs with pace, HR zones, and training effect score.
```

### Sleep and recovery monitoring tool

A holistic wellness app tracks athlete recovery by combining Garmin's nightly sleep data, HRV status, and body battery levels with training load. Garmin pushes daily summary data including sleep scores and HRV readings. The dashboard visualizes the relationship between training load and recovery, helping athletes identify when they need extra rest.

Prompt example:

```
Build a recovery tracking dashboard using Garmin data. The Garmin webhook receives daily summaries including sleep score, HRV status, body battery start/end, and stress score. Store in a daily_summaries table. Display: 7-day sleep score trend, HRV baseline and current reading, body battery morning level over 30 days, correlation view showing training load vs. next-day HRV.
```

## Troubleshooting

### Garmin webhook sends data but it never appears in Supabase tables

Cause: The webhook receiver Edge Function is returning 200 but failing silently during data processing, or the Garmin user ID isn't found in the garmin_user_mapping table so all records are skipped.

Solution: Add detailed logging to your webhook receiver: console.log the incoming payload structure and each stage of processing. Check Cloud Logs immediately after recording a Garmin activity. Verify the garmin_user_mapping table has entries linking Garmin user IDs to your app's user IDs — without this mapping, all incoming data is skipped.

```
// Log at each step for debugging
console.log('Webhook received:', JSON.stringify(body).substring(0, 200));
console.log('Activity count:', body.activitySummaries?.length ?? 0);
```

### OAuth 1.0a signed requests to Garmin API return 401

Cause: Garmin's OAuth 1.0a signing requires both Consumer Secret AND OAuth Token Secret in the signing key. A common mistake is using only the Consumer Secret (correct for app-level auth) when making user-level requests that require the user's token secret as well.

Solution: For requests on behalf of a specific user, the signing key must be: CONSUMER_SECRET + '&' + OAUTH_TOKEN_SECRET. Retrieve the user's oauth_token_secret from the garmin_tokens table and include it in the signing key for all user-specific API calls. For requests not on behalf of a specific user, use: CONSUMER_SECRET + '&' (empty token secret).

```
const signingKey = `${CONSUMER_SECRET}&${oauthTokenSecret ?? ''}`;
```

### Garmin never sends webhook data after registration

Cause: Garmin's webhook registration requires the URL to be publicly accessible and respond to a validation GET request before Garmin will start pushing data. If the Edge Function URL isn't reachable or doesn't handle GET requests, Garmin may not activate the webhook.

Solution: Ensure your Edge Function handles both GET and POST methods. GET requests from Garmin's validation may include a challenge parameter that must be echoed back. Test the webhook URL is publicly accessible using a tool like reqbin.com. Check the Garmin developer console for webhook registration status and any error messages.

```
if (req.method === "GET") {
  const url = new URL(req.url);
  const challenge = url.searchParams.get("challenge");
  return new Response(JSON.stringify({ challenge }), { headers: { "Content-Type": "application/json" } });
}
```

## Frequently asked questions

### Why does Garmin use a push model instead of a pull API like Fitbit?

Garmin's push model is designed for the typical Garmin use case: a user goes for a run, syncs their watch when they return, and the data is immediately pushed to all connected apps. This is more efficient than apps polling Garmin repeatedly waiting for new data. The tradeoff is that your app needs a publicly accessible webhook endpoint rather than simply making GET requests when the user loads your dashboard.

### Can I backfill historical Garmin data for a new connection?

Yes — Garmin provides a backfill API that lets you request historical data for a specified date range when a user first connects. Call GET /wellness-api/rest/backfill/activities with start and end timestamps and Garmin will push the historical data to your webhook endpoint. This is useful for populating charts on first connection rather than starting from an empty history.

### What's the difference between Garmin Health API tiers?

Garmin offers multiple API tiers: Daily Summary API (steps, calories, activity minutes), Activity Details API (GPS tracks, lap data, interval data), Wellness Summary API (sleep, stress, HRV, body battery), Body Composition API (weight, body fat, muscle mass from Garmin Index scales), and Device Information API. Access to each tier depends on your developer agreement. Start with the Daily Summary and Activity Details tiers — these cover most use cases.

### How do I handle Garmin users who don't sync their watch regularly?

Garmin devices typically sync when connected to the Garmin Connect mobile app via Bluetooth. If a user doesn't sync for several days, Garmin may send multiple days of data in a single webhook batch when they finally sync. Your webhook receiver should handle batches with multiple days of daily summaries by processing each record with its specific date, rather than assuming all data is for 'today'.

---

Source: https://www.rapidevelopers.com/lovable-integration/garmin-connect
© RapidDev — https://www.rapidevelopers.com/lovable-integration/garmin-connect
