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:
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:
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¶
- A SweatStack account (app.sweatstack.no)
- uv installed
- Fly.io CLI installed (for deployment)
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:
- Go to app.sweatstack.no/applications/new.
- Fill in the app details (placeholder values are fine for now).
- Set the redirect URI to
http://localhost:8000/auth/sweatstack/callback. - 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:
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:
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>
"""
- Reads credentials from environment variables automatically.
- Adds login, logout, and callback routes to your app.
OptionalUserprovides the user if logged in, orNoneotherwise.
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:
@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>
"""
user.clientis a pre-configured SweatStack client for API calls. See the Python client docs for available methods.
View complete code
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.
-
Create the Fly app.
fly launchFly auto-detects FastAPI and creates a
fly.tomlandDockerfile. Accept the defaults or customize as needed. -
Update the redirect URI. In your SweatStack app settings, add a new redirect URI:
https://YOUR_APP.fly.dev/auth/sweatstack/callbackReplace
YOUR_APPwith your Fly.io app name. -
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 -
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}
- 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}
- 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¶
- Browse the HTTP API reference for every endpoint.
- The FastAPI framework reference documents every
sweatstack.fastapisymbol. - Add a frontend with HTMX, React, or Vue.
- Subscribe to webhooks to react to new activities in real time.