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:
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:
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¶
- A SweatStack account (app.sweatstack.no)
- uv installed
- The Fly.io CLI installed, for the deployment
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:
- Go to app.sweatstack.no/applications/new.
- Enter a name. Leave the public profile fields for later.
- Set the redirect URI to
http://localhost:8000/auth/sweatstack/callback. - Save.
- 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:
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:
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>
"""
- Reads the credentials from the environment variables.
- Adds the login, logout, and callback routes to your app.
OptionalUseris the user when they are signed in, andNoneotherwise.
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:
@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>
"""
user.clientis a configured SweatStack client for API calls. See the Python SDK reference for the methods.durationis atimedelta.summary.distanceis absent on an activity that recorded no distance, so guard for it. See summary statistics.
View the 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)
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.
-
Create the Fly app.
fly launchFly detects FastAPI and creates a
fly.tomland aDockerfile. Accept the defaults, or customize them. -
Add the production redirect URI. In your SweatStack app's settings, add:
https://YOUR_APP.fly.dev/auth/sweatstack/callbackReplace
YOUR_APPwith your Fly.io app name. -
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 -
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}
- 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}
- 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.fastapisymbol. - Add a frontend with HTMX, React, or Vue.
- Subscribe to webhooks to react to new activities as they arrive.