Skip to content

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:

  1. Client ID and secret. Create an application and set the redirect URI to:

    http://localhost:8000/auth/sweatstack/callback
    

  2. 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)!
  1. Configures the OAuth credentials. In production, use environment variables instead.
  2. Adds the login, logout, and callback routes to your app. See Routes added for the full list.
  3. OptionalUser works for signed-in and anonymous visitors. See Protecting routes.
  4. AuthenticatedUser requires login. The helper redirects anonymous visitors to the login route.
  5. Every user object carries a configured client for 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:

  1. Not signed in. The helper redirects them to /auth/sweatstack/login.
  2. 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()
  1. The helper redirects anonymous visitors to the login route. To return a 401 instead, set redirect_unauthenticated=False in 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!"}
  1. user is None when 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>")
  1. Returns the users who have granted this coach access to their data.
  2. Use both dependencies together: user is whoever is selected, principal is always the signed-in coach.
  3. 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)!
  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)!
)
  1. The OAuth scopes to request. The defaults cover most use cases.
  2. The session lifetime in seconds. The default is 24 hours.
  3. The prefix of every auth route.
  4. Set it to False to return a 401 instead of redirecting to the login route.
  5. 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