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 through 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 an 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
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)
def home(user: OptionalUser):
    if not user:
        return '<h1>Training Dashboard</h1><a href="/auth/sweatstack/login">Login</a>'

    activities = user.client.activities.list(limit=7)
    total_hours = sum(a.duration.total_seconds() 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 it 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

Then add https://YOUR_APP.fly.dev/auth/sweatstack/callback to the redirect URIs of your SweatStack app.

Prerequisites

Set up the project

Create a project and install the 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. Enter a name. Leave the public profile fields for later.
  3. Set the redirect URI to http://localhost:8000/auth/sweatstack/callback.
  4. Save.
  5. Click Create Secret and copy the secret immediately. SweatStack shows it only once.

Generate a session secret. The helper uses it to encrypt the session cookie:

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 you commit 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)
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 the credentials from the environment variables.
  2. Adds the login, logout, and callback routes to your app.
  3. OptionalUser is the user when they are signed in, and None otherwise.

Run locally

uv run --env-file .env fastapi dev

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

Add activity data

Show the user's recent training. Update the home route:

main.py
@app.get("/", response_class=HTMLResponse)
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 = user.client.activities.list(limit=7)  # (1)!

    total_hours = sum(a.duration.total_seconds() for a in activities) / 3600
    total_km = sum(
        a.summary.distance.sum for a in activities if a.summary and a.summary.distance
    ) / 1000  # (2)!

    activity_list = "".join(
        f"<li>{a.sport} - {int(a.duration.total_seconds() // 60)} min</li>"
        for a in activities
    )

    return f"""
    <h1>Your Recent Training</h1>
    <p><strong>{len(activities)}</strong> activities</p>
    <p><strong>{total_hours:.1f}</strong> hours</p>
    <p><strong>{total_km:.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 configured SweatStack client for API calls. See the Python SDK reference for the methods.
  2. duration is a timedelta. summary.distance is absent on an activity that recorded no distance, so guard for it. See summary statistics.
View the 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)
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 = user.client.activities.list(limit=7)

    total_hours = sum(a.duration.total_seconds() for a in activities) / 3600
    total_km = sum(
        a.summary.distance.sum for a in activities if a.summary and a.summary.distance
    ) / 1000

    activity_list = "".join(
        f"<li>{a.sport} - {int(a.duration.total_seconds() // 60)} min</li>"
        for a in activities
    )

    return f"""
    <h1>Your Recent Training</h1>
    <p><strong>{len(activities)}</strong> activities</p>
    <p><strong>{total_hours:.1f}</strong> hours</p>
    <p><strong>{total_km:.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 as a concrete example. Any container host works the same way. The only host-specific parts are the launch command, the secrets command, and the public URL.

  1. Create the Fly app.

    fly launch
    

    Fly detects FastAPI and creates a fly.toml and a Dockerfile. Accept the defaults, or customize them.

  2. Add the production redirect URI. In your SweatStack app's settings, add:

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

    Replace YOUR_APP with your Fly.io app name.

  3. Set the 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 live at https://YOUR_APP.fly.dev.

Going further

Routes added by instrument()

instrument(app) adds these routes:

Route Method Description
/auth/sweatstack/login GET Starts the OAuth flow
/auth/sweatstack/callback GET Completes the OAuth flow
/auth/sweatstack/logout POST Ends the session
/auth/sweatstack/select-user/{user_id} POST Switches to another user, for coaches
/auth/sweatstack/select-self POST Switches back to the coach's own data

Protecting routes

Use AuthenticatedUser when a route requires login:

from sweatstack.fastapi import AuthenticatedUser

@app.get("/dashboard")
def dashboard(user: AuthenticatedUser):  # (1)!
    activities = user.client.activities.list(limit=10)
    return {"activities": activities}
  1. The helper redirects visitors who are not signed in to the login route.

Coach and athlete switching

If you're building for coaches, use SelectedUser to read the selected athlete's data:

from sweatstack.fastapi import SelectedUser

@app.get("/athlete-dashboard")
def athlete_dashboard(user: SelectedUser):  # (1)!
    activities = user.client.activities.list(limit=10)
    return {"activities": activities}
  1. The selected athlete, or the coach themselves when no athlete is selected.

More client examples

# The signed-in user's profile
info = user.client.oauth.userinfo()

# List activities with filters
from datetime import date

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

# The timeseries of one activity
data = user.client.activities.data(activity_id)

See the Python SDK reference for the full method list.

What's next

  • Browse the API reference for every endpoint and response schema. Run requests live in the API playground.
  • The FastAPI helper page documents every sweatstack.fastapi symbol.
  • Add a frontend with HTMX, React, or Vue.
  • Subscribe to webhooks to react to new activities as they arrive.