Oliver
Post

Shifting to Strava

Sunday 04 May 2025

Automatically syncing Strava activities to my Supabase DB!

For the last couple of years, I’ve been using my own system to keep track of my runs, inspired by Josh Crain’s Running Log.
However, it became so incredibly convoluted that I started to lose the will to live.

To go for a run, I had to:

  • Start the C25K app to provide something vaguely resembling a running commentary.
  • Set the screen lock time to 30 minutes, or I’d end up desperately swiping at the screen mid-run.
  • Open Google Fit (an app I cannot overstate my dislike for) and set it to track the run.
  • Start Spotify with the correct playlist.

And then, at the end, I had to go back and close/reset/save each of those apps, then copy the relevant information from Google Fit onto my online form. I almost spent more time fiddling with my phone than actually running.

The worst part? If I paused for more than a moment, I’d have to manually pause both Google Fit and C25K, then resume them a few seconds later. It was all horrifically cumbersome and complicated — I was sick of it.

One of my friends recommended Strava for its social features. After trying it for a couple of runs, I decided it looked promising! However, I didn’t want to end up with all my data locked in Strava — I’d already built a pretty chunky database of runs, and I wanted new Strava runs to sync with that (crucially, without me having to do anything).

So, I wrote a quick Python script to copy recent data from Strava into Supabase (see the bottom of this article!). In my earlier setup, I could add custom fields like “Did you run with the dog?”, but Strava doesn’t support custom input like that. I needed a new way to capture extra data.

After thinking it over, I tweaked the script to send myself an email with a link to a short online form, where I could fill in any additional info. And just like that, the system was perfect!

Now, after a run, I simply save the Strava activity. A couple of hours later, I get an email asking for more details. I fill out the form, the data is pulled into the database, and this website is automatically rebuilt!

I know this is a bit rambly — if you have any questions, feel free to drop me a line. Otherwise, enjoy browsing the code below!

The code

StravaSync Python Script

# Initialize Supabase client
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart


def send_email(run_id, date, distance):
try:
print("Trying to send email...")
subject = "🏃‍♂️ Finish adding run"
# Read the HTML template
with open("emailTemplate.html", "r", encoding="utf-8") as file:
body = file.read()

# Replace placeholders
body = body.replace("{{run_id}}", run_id)
body = body.replace("{{date}}", str(date))
body = body.replace("{{distance}}", str(round(distance, 2)))

msg = MIMEMultipart()
msg["Subject"] = subject
msg["From"] = EMAIL_SENDER
msg["To"] = EMAIL_RECEIVER
msg.attach(MIMEText(body, "html"))

# Establish SMTP connection
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
server.starttls()
server.login(EMAIL_SENDER, EMAIL_PASSWORD)

# Send the email
server.sendmail(EMAIL_SENDER, EMAIL_RECEIVER, msg.as_string())
print("Email sent successfully!")

# Ensure the connection is properly closed
server.quit()

except smtplib.SMTPException as e:
print(f"SMTP error occurred: {e}")
except Exception as e:
print(f"Failed to send email: {e}")


# Function to refresh Strava token
def refresh_strava_token():
url = "https://www.strava.com/oauth/token"
payload = {
"client_id": STRAVA_CLIENT_ID,
"client_secret": STRAVA_CLIENT_SECRET,
"refresh_token": STRAVA_REFRESH_TOKEN,
"grant_type": "refresh_token",
}
response = requests.post(url, data=payload)
response_data = response.json()
print(response_data["access_token"])
return response_data["access_token"]


# Function to fetch new Strava activities
def get_strava_activities():
access_token = refresh_strava_token()
headers = {"Authorization": f"Bearer {access_token}"}
url = "https://www.strava.com/api/v3/athlete/activities"

# Get activities from the last 4 hours
four_hours_ago = datetime.utcnow() - timedelta(hours=4)
params = {"after": int(four_hours_ago.timestamp())}

response = requests.get(url, headers=headers, params=params)
return response.json() if response.status_code == 200 else []


# Function to format and insert data into Supabase
def add_run():
try:
activities = get_strava_activities()

if not activities:
print("No new activities found.")
return

for activity in activities:
print(activity)
if activity["type"] != "Run":
continue # Skip non-running activities

date = datetime.strptime(activity["start_date_local"], '%Y-%m-%dT%H:%M:%SZ').date()
start_time = datetime.strptime(activity["start_date_local"], '%Y-%m-%dT%H:%M:%SZ').time()
duration_seconds = activity["moving_time"]
distance = activity["distance"] / 1000 # Convert meters to km
steps = activity.get("steps", 0) # Strava may not provide steps, default to 0
calories = activity.get("calories", 0)
try:
heart_points = int(activity.get("suffer_score", None)) # Strava doesn't have direct heart points
except:
heart_points = 0
dog = "#dogrun" in activity.get("description", "").lower()
notes = activity.get("name", "") # Use activity name as notes
run_id = ''.join(random.choices(string.ascii_letters + string.digits, k=9))
day_of_week = calendar.day_name[date.weekday()]

# Calculate pace
pace_seconds_per_km = duration_seconds / distance if distance > 0 else 0
pace_minutes = int(pace_seconds_per_km // 60)
pace_remaining_seconds = int(pace_seconds_per_km % 60)
pace = f"{pace_minutes}:{pace_remaining_seconds:02d}"

# Calculate stride length (meters per step)
stride = (distance * 1000) / steps if steps > 0 else 0

# Insert into Supabase
supabase.table("runningData").insert({
"Date": date.strftime('%Y-%m-%d'),
"ID": run_id,
"Day": day_of_week,
"StartTime": start_time.strftime('%H:%M'),
"EndTime": (datetime.combine(date, start_time) + timedelta(seconds=duration_seconds)).strftime('%H:%M'),
"Duration": f"{int(duration_seconds // 60):02}:{int(duration_seconds % 60):02}",
"Distance": distance,
"Pace": pace,
"Stride": round(stride, 2),
"Calories": calories,
"Steps": steps,
"HeartPoints": heart_points,
"Dog": dog,
"Comments": '',
"dataFormat": 'stravaImporterV1'
}).execute()

print(f"Added run: {run_id}")
send_email(run_id, date, distance)

except Exception as e:
print(f"An error occurred: {e}")


# Run the script!
add_run()

Online Form

@app.route('/addDetail/<runID>', methods=['GET', 'POST'])
def addDetail(runID):
if request.method == 'POST':
try:
dogState = request.form['dog']
if dogState == 'on':
dogState = True
else:
dogState = False
except:
dogState = False
try:
comments = request.form['note']
except:
comments = ''
print(dogState)
print(comments)
(
supabase.table("runningData")
.update(
{
"Dog": dogState,
"Comments": comments,
"dataComplete": True
}
)
.eq("ID", runID)
.execute()
)

requests.post('{{ CLOUDFLARE_PAGES_WEBHOOK }}')
return render_template('detailSuccess.html')
else:
try:
response = (
supabase.table("runningData")
.select("Date, Distance, dataComplete")
.eq("ID", runID)
.execute()
)
runData = response.data
dataComplete = runData[0]["dataComplete"]
distance=runData[0]["Distance"]
date=runData[0]["Date"]
print(dataComplete)
print(response)
return render_template('runDetail.html', hideForm=dataComplete, distance=distance, date=date)
except Exception as error:
print(error)
return render_template('runDetail.html', hideForm=True)

Email

Look! It's pretty! Why not build your own...?!

Running Email