Skip to content

Build and deploy a FastAPI app

End-to-end: a FastAPI app that authenticates against SweatStack, queries activity data, and ships to Fly.io with a public URL.

What you'll build

A FastAPI app that:

  • Authenticates users with SweatStack via OAuth2
  • Queries activity data through 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

Set up the project

Create a new project and install dependencies:

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

Create a SweatStack application

Register your app to get OAuth2 credentials:

  1. Go to app.sweatstack.no/applications/new.
  2. Fill in the app details (placeholder values are fine for now).
  3. Set the redirect URI to http://localhost:8000/auth/sweatstack/callback.
  4. Save, then click Create Secret and copy it immediately. It's shown only once.

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

Warning

Add .env to .gitignore before committing anything.

Build the app

Create main.py:

main.py
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.

Add activity data

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
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

This guide uses Fly.io for concreteness. Any container host works the same way; the only host-specific bits are the launch command, the secrets command, and the eventual public URL.

  1. Create the Fly app.

    fly launch
    

    Fly auto-detects FastAPI and creates a fly.toml and Dockerfile. Accept the defaults or customize as needed.

  2. Update the 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
    

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

Going further

Routes added by instrument()

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 another user (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 the selected athlete's data, or the coach's own if none selected.

More client examples

# OAuth userinfo for the authenticated user
info = user.client.get_userinfo()

# List activities with filters (note: start/end dates, sports as a list)
from datetime import date

activities = user.client.get_activities(
    start=date(2026, 1, 1),
    end=date(2026, 1, 31),
    sports=["cycling"],
)

# Get timeseries data for a specific activity
data = user.client.get_activity_data(activity_id)

See the Python client reference for the full method list.

What's next