FastAPI¶
Add SweatStack to your FastAPI app in three steps. After setup, every authenticated request gives you a ready-to-use API client. You manage no tokens.
1. Install¶
uv add 'sweatstack[fastapi]'
2. Get your credentials¶
Before you write code, get two things:
-
Client ID and secret. Create an application and set the redirect URI to:
http://localhost:8000/auth/sweatstack/callback -
Session secret. Generate a Fernet key, used to encrypt the session cookie:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
3. Create your app¶
Create app.py:
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from sweatstack.fastapi import (
configure, instrument, AuthenticatedUser, OptionalUser, urls
)
configure( # (1)!
client_id="your-client-id",
client_secret="your-client-secret",
session_secret="your-generated-fernet-key",
app_url="http://localhost:8000",
)
app = FastAPI()
instrument(app) # (2)!
@app.get("/")
def home(user: OptionalUser): # (3)!
if user:
return HTMLResponse(f"""
Welcome, {user.user_id}!
<a href='/activities'>My activities</a>
<form method='POST' action='{urls.logout()}'><button>Logout</button></form>
""")
return HTMLResponse(f"<a href='{urls.login()}'>Login with SweatStack</a>")
@app.get("/activities")
def activities(user: AuthenticatedUser): # (4)!
return user.client.activities.list(limit=10) # (5)!
- Configures the OAuth credentials. In production, use environment variables instead.
- Adds the login, logout, and callback routes to your app. See Routes added for the full list.
OptionalUserworks for signed-in and anonymous visitors. See Protecting routes.AuthenticatedUserrequires login. The helper redirects anonymous visitors to the login route.- Every user object carries a configured
clientfor API calls. See the Python SDK reference for the methods.
Run it:
uv run fastapi dev app.py
Open http://localhost:8000. Click the login link, sign in with SweatStack, and you're back with full API access.
How it works¶
Routes added¶
instrument() adds these routes to your app:
| Route | Purpose |
|---|---|
GET /auth/sweatstack/login |
Starts OAuth flow |
GET /auth/sweatstack/callback |
Completes OAuth flow |
POST /auth/sweatstack/logout |
Ends the session |
POST /auth/sweatstack/select-user/{user_id} |
Switch to another user's view |
POST /auth/sweatstack/select-self |
Switch back to your own view |
What happens on each request¶
When a user visits a protected route:
- Not signed in. The helper redirects them to
/auth/sweatstack/login. - Signed in. Your handler receives a user object with:
user.user_id: the signed-in user's ID.user.client: a configured SweatStack client, ready for API calls.
Tokens live in encrypted, httponly cookies. You need no database.
Protecting routes¶
Pick the dependency by whether the route requires login:
AuthenticatedUser (login required)¶
from sweatstack.fastapi import AuthenticatedUser
@app.get("/dashboard")
def dashboard(user: AuthenticatedUser): # (1)!
return user.client.activities.list()
- The helper redirects anonymous visitors to the login route. To return a
401instead, setredirect_unauthenticated=Falsein configure().
OptionalUser (login optional)¶
from sweatstack.fastapi import OptionalUser
@app.get("/")
def home(user: OptionalUser): # (1)!
if user:
return {"message": f"Hello, {user.user_id}!"}
return {"message": "Hello, guest!"}
userisNonewhen the visitor is not signed in. Otherwise it is a full user object.
Building login/logout UI¶
Use the urls helper to generate auth URLs:
from fastapi.responses import HTMLResponse
from sweatstack.fastapi import OptionalUser, urls
@app.get("/")
def home(user: OptionalUser):
if user:
return HTMLResponse(f"""
<p>Welcome, {user.user_id}!</p>
<form method="POST" action="{urls.logout()}">
<button>Logout</button>
</form>
""")
return HTMLResponse(f"""
<a href="{urls.login()}">Login with SweatStack</a>
""")
Available URL helpers¶
| Method | Returns |
|---|---|
urls.login() |
/auth/sweatstack/login |
urls.login(next="/dashboard") |
Login, then redirect to /dashboard |
urls.logout() |
/auth/sweatstack/logout |
urls.select_user(user_id) |
Switch to viewing another user |
urls.select_user(user_id, next="/dashboard") |
Switch user, then redirect |
urls.select_self() |
Switch back to your own view |
The next parameter sets where the helper redirects the user after the action completes.
User switching (delegation)¶
For coaching platforms and multi-user apps, SweatStack supports viewing data on behalf of another user. The signed-in user (the coach) can switch to an athlete's data while staying signed in as themselves.
Additional dependencies¶
Besides AuthenticatedUser and OptionalUser, delegation adds:
| Dependency | Returns |
|---|---|
AuthenticatedUser |
Always the signed-in user (the coach) |
SelectedUser |
The user currently viewed (the athlete, or the coach themselves) |
OptionalSelectedUser |
The same, but None when nobody is signed in |
Example: coach dashboard¶
from sweatstack.fastapi import AuthenticatedUser, SelectedUser, urls
from fastapi.responses import HTMLResponse
@app.get("/athletes")
def athletes(user: AuthenticatedUser):
accessible = user.client.users.list() # (1)!
links = "".join(
f"""<form method="post" action="{urls.select_user(u.id, next='/dashboard')}">
<button>{u.display_name}</button>
</form>"""
for u in accessible if u.id != user.user_id
)
return HTMLResponse(f"<h1>My Athletes</h1>{links}")
@app.get("/dashboard")
def dashboard(user: SelectedUser, principal: AuthenticatedUser): # (2)!
activities = user.client.activities.list(limit=5)
back_btn = ""
if user.user_id != principal.user_id: # (3)!
back_btn = f"""<form method="post" action="{urls.select_self()}">
<button>Back to my view</button>
</form>"""
return HTMLResponse(f"{back_btn}<pre>{activities}</pre>")
- Returns the users who have granted this coach access to their data.
- Use both dependencies together:
useris whoever is selected,principalis always the signed-in coach. - Show a "back" button only when the coach is viewing someone else's data.
Configuration¶
Environment variables¶
In production, read the secrets from environment variables instead of writing them in code:
export SWEATSTACK_CLIENT_ID="your-client-id"
export SWEATSTACK_CLIENT_SECRET="your-client-secret"
export SWEATSTACK_SESSION_SECRET="your-fernet-key"
export APP_URL="https://yourapp.com"
Then call configure() with no arguments:
configure() # (1)!
- Reads every value from the environment variables.
| Variable | Description |
|---|---|
SWEATSTACK_CLIENT_ID |
OAuth client ID |
SWEATSTACK_CLIENT_SECRET |
OAuth client secret |
SWEATSTACK_SESSION_SECRET |
Fernet key for cookie encryption |
APP_URL |
Your app's public URL |
All options¶
configure(
# Required (or use environment variables)
client_id="...",
client_secret="...",
session_secret="...",
app_url="http://localhost:8000",
# Optional
scopes=["profile", "data:read"], # (1)!
cookie_max_age=86400, # (2)!
auth_route_prefix="/auth/sweatstack", # (3)!
redirect_unauthenticated=True, # (4)!
access_token_cache=None, # (5)!
)
- The OAuth scopes to request. The defaults cover most use cases.
- The session lifetime in seconds. The default is 24 hours.
- The prefix of every auth route.
- Set it to
Falseto return a401instead of redirecting to the login route. - A custom access-token cache for multi-worker deployments. See Concurrent refresh handling. Single-worker apps can leave it
None.
Concurrent refresh handling¶
When a page loads several requests in parallel and the session's access token is about to expire, the helper collapses the burst of refreshes into a single /oauth/token call. The other requests wait briefly for the in-flight refresh and reuse its result. This is on by default. A single-worker deployment needs no configuration.
Three things to know if you run more than one worker:
Multi-worker deployments. The default in-process cache de-duplicates within each worker, not across workers. If you run several FastAPI workers (uvicorn --workers 4, Gunicorn, and so on) and want cross-worker de-duplication, plug in a shared-state implementation:
from sweatstack.fastapi import configure, AccessTokenCache
class RedisAccessTokenCache(AccessTokenCache):
# Implement: get, set, invalidate, migrate, lock
# See sweatstack/fastapi/access_token_cache.py for the Protocol surface.
...
configure(..., access_token_cache=RedisAccessTokenCache(redis_client))
Timeouts. The token endpoint call times out after 10 seconds. A request that can't acquire the per-session refresh lock within 15 seconds raises RefreshLockTimeout, which the helper turns into a 401 in browser flows and a WebhookTokenRefreshError in webhook flows. You don't need to catch these unless you want to send them to your metrics.
Refresh-token rotation. If SweatStack starts rotating refresh tokens server-side, the helper handles it: it writes the new refresh token to the cookie and updates the cache under both the old and the new key, so in-flight requests still find it.
Debugging. Enable debug logs to see every cache decision. The log keys on a short SHA-256 fingerprint of the refresh token, never on the raw secret:
import logging
logging.getLogger("sweatstack.fastapi.dependencies").setLevel(logging.DEBUG)
Next steps¶
- Browse the API reference for every endpoint and response schema, or open the API playground to run requests live.
- Read the Python SDK reference for every client method.
- Read OAuth2 for what the helper does underneath.