Skip to content

Deploy a FastAPI App to Fly.io

Build and deploy an authenticated API with SweatStack fitness data using FastAPI and Fly.io.

What You'll Build

A FastAPI app that:

  • Authenticates users with SweatStack OAuth
  • Fetches activity data via the SweatStack API
  • Deploys to Fly.io with a public URL
In a hurry?
uv init fastapi-app && cd fastapi-app
uv add 'sweatstack[fastapi]'

Create a new app at app.sweatstack.no with redirect URI http://localhost:8000/auth/sweatstack/callback.

Generate a session secret:

python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Create .env:

.env
SWEATSTACK_CLIENT_ID=your_client_id
SWEATSTACK_CLIENT_SECRET=your_client_secret
SWEATSTACK_SESSION_SECRET=your_generated_session_secret
APP_URL=http://localhost:8000

Create main.py:

main.py
import os
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from sweatstack.fastapi import configure, instrument, OptionalUser

app = FastAPI(title="Training Dashboard")

configure()
instrument(app)

@app.get("/", response_class=HTMLResponse)
async def home(user: OptionalUser):
    if not user:
        return '<h1>Training Dashboard</h1><a href="/auth/sweatstack/login">Login</a>'

    activities = await user.client.get_activities(limit=7)
    total_hours = sum(a.duration_seconds or 0 for a in activities) / 3600

    return f'''
    <h1>Your Week</h1>
    <p><strong>{len(activities)}</strong> activities, <strong>{total_hours:.1f}</strong> hours</p>
    <form action="/auth/sweatstack/logout" method="post"><button>Logout</button></form>
    '''

Run locally:

uv run --env-file .env fastapi dev

Deploy to Fly.io:

fly launch
fly secrets set SWEATSTACK_CLIENT_ID=... SWEATSTACK_CLIENT_SECRET=... SWEATSTACK_SESSION_SECRET=... APP_URL=https://YOUR_APP.fly.dev
fly deploy

Don't forget to update the redirect URI in your SweatStack app to https://YOUR_APP.fly.dev/auth/sweatstack/callback.

Prerequisites

Project Setup

Create a new project and install dependencies:

uv init fastapi-app
cd fastapi-app
uv add 'sweatstack[fastapi]'

Creating a SweatStack App

Register your app with SweatStack to get API credentials:

  1. Go to app.sweatstack.no
  2. Fill in the app details (use placeholder values for now)
  3. Set the redirect URI to http://localhost:8000/auth/sweatstack/callback
  4. Save and click "Create Secret" - copy it immediately

Generate a session secret (used to encrypt cookies):

python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Create a .env file with your credentials:

.env
SWEATSTACK_CLIENT_ID=your_client_id
SWEATSTACK_CLIENT_SECRET=your_client_secret
SWEATSTACK_SESSION_SECRET=your_generated_session_secret
APP_URL=http://localhost:8000

Security

Add .env to your .gitignore to keep your secrets safe!

Creating the FastAPI App

Create main.py:

main.py
import os
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from sweatstack.fastapi import configure, instrument, OptionalUser

app = FastAPI(title="Training Dashboard")

configure()  # (1)!
instrument(app)  # (2)!

@app.get("/", response_class=HTMLResponse)
async def home(user: OptionalUser):  # (3)!
    if not user:
        return """
        <h1>Training Dashboard</h1>
        <p>View your weekly training summary.</p>
        <a href="/auth/sweatstack/login">Login with SweatStack</a>
        """

    return f"""
    <h1>Welcome!</h1>
    <p>You're logged in as user {user.user_id}</p>
    <form action="/auth/sweatstack/logout" method="post">
        <button type="submit">Logout</button>
    </form>
    """
  1. Reads credentials from environment variables automatically.
  2. Adds login, logout, and callback routes to your app.
  3. OptionalUser provides the user if logged in, or None otherwise.

Run Locally

uv run --env-file .env fastapi dev

Open http://localhost:8000 and click Login with SweatStack to test authentication.

Adding Activity Data

Let's display the user's recent training. Update the home route:

main.py
@app.get("/", response_class=HTMLResponse)
async def home(user: OptionalUser):
    if not user:
        return """
        <h1>Training Dashboard</h1>
        <p>View your weekly training summary.</p>
        <a href="/auth/sweatstack/login">Login with SweatStack</a>
        """

    activities = await user.client.get_activities(limit=7)  # (1)!

    total_duration = sum(a.duration_seconds or 0 for a in activities) / 3600
    total_distance = sum(a.distance_meters or 0 for a in activities) / 1000

    activity_list = "".join(
        f"<li>{a.sport} - {(a.duration_seconds or 0) // 60} min</li>"
        for a in activities
    )

    return f"""
    <h1>Your Recent Training</h1>
    <p><strong>{len(activities)}</strong> activities</p>
    <p><strong>{total_duration:.1f}</strong> hours</p>
    <p><strong>{total_distance:.1f}</strong> km</p>
    <ul>{activity_list}</ul>
    <form action="/auth/sweatstack/logout" method="post">
        <button type="submit">Logout</button>
    </form>
    """
  1. user.client is a pre-configured SweatStack client for API calls. See the Python client docs for available methods.
View complete code
main.py
import os
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from sweatstack.fastapi import configure, instrument, OptionalUser

app = FastAPI(title="Training Dashboard")

configure()
instrument(app)


@app.get("/", response_class=HTMLResponse)
async def home(user: OptionalUser):
    if not user:
        return """
        <h1>Training Dashboard</h1>
        <p>View your weekly training summary.</p>
        <a href="/auth/sweatstack/login">Login with SweatStack</a>
        """

    activities = await user.client.get_activities(limit=7)

    total_duration = sum(a.duration_seconds or 0 for a in activities) / 3600
    total_distance = sum(a.distance_meters or 0 for a in activities) / 1000

    activity_list = "".join(
        f"<li>{a.sport} - {(a.duration_seconds or 0) // 60} min</li>"
        for a in activities
    )

    return f"""
    <h1>Your Recent Training</h1>
    <p><strong>{len(activities)}</strong> activities</p>
    <p><strong>{total_duration:.1f}</strong> hours</p>
    <p><strong>{total_distance:.1f}</strong> km</p>
    <ul>{activity_list}</ul>
    <form action="/auth/sweatstack/logout" method="post">
        <button type="submit">Logout</button>
    </form>
    """

Deploy to Fly.io

1. Create Fly App

fly launch

This auto-detects FastAPI and creates:

  • fly.toml - Fly.io configuration
  • Dockerfile - Container build instructions

Accept the defaults or customize as needed.

2. Update Redirect URI

In your SweatStack app settings, add a new redirect URI:

https://YOUR_APP.fly.dev/auth/sweatstack/callback

Replace YOUR_APP with your Fly.io app name.

3. Set Secrets

fly secrets set \
  SWEATSTACK_CLIENT_ID=your_client_id \
  SWEATSTACK_CLIENT_SECRET=your_client_secret \
  SWEATSTACK_SESSION_SECRET=your_session_secret \
  APP_URL=https://YOUR_APP.fly.dev

4. Deploy

fly deploy

Your app is now live at https://YOUR_APP.fly.dev

Building Your Own App

Available Routes

The instrument(app) call adds these routes:

Route Method Description
/auth/sweatstack/login GET Start OAuth flow
/auth/sweatstack/callback GET OAuth callback
/auth/sweatstack/logout POST End session
/auth/sweatstack/select-user/{user_id} POST Switch to athlete (for coaches)
/auth/sweatstack/select-self POST Return to own data

Protecting Routes

Use AuthenticatedUser when login is required:

from sweatstack.fastapi import AuthenticatedUser

@app.get("/dashboard")
async def dashboard(user: AuthenticatedUser):  # (1)!
    activities = await user.client.get_activities(limit=10)
    return {"activities": activities}
  1. Unauthenticated users are redirected to login automatically.

Coach/Athlete Switching

If you're building for coaches, use SelectedUser to access athlete data:

from sweatstack.fastapi import SelectedUser

@app.get("/athlete-dashboard")
async def athlete_dashboard(user: SelectedUser):  # (1)!
    activities = await user.client.get_activities(limit=10)
    return {"activities": activities}
  1. Returns selected athlete's data, or coach's own if none selected.

More API Examples

# Get athlete profile
profile = await user.client.get_athlete()

# List activities with filters
activities = await user.client.get_activities(
    start_date="2024-01-01",
    end_date="2024-01-31",
    sport="cycling"
)

# Get activity streams (time-series data)
streams = await user.client.get_activity_streams(activity_id)

Next Steps