Streamlit
Sign users in with their UseThatApp account and read their live plan from a Streamlit app, over OAuth 2.0 / OpenID Connect. Streamlit has no request/route model, so it can't be a confidential OIDC client on its own. The realistic pattern is a small companion server (a backend-for-frontend, or BFF) that runs the SDK and handles /login, /callback, and /logout; Streamlit reads the resulting session and entitlement from it. The same BFF wires your app into the whole selling loop: a buyer who purchases from your own website lands back signed in, entitlement already live — and marketplace launches arrive the same way. For the conceptual model, see OpenID Connect.
⚠ This is a sidecar / BFF setup.
The usethatapp SDK is a confidential client — it holds the client secret and validates ID tokens, so it must run server-side. A Streamlit script restarts top-to-bottom on every interaction and has no place to receive the OAuth redirect, so the OIDC flow lives in a small companion server (Flask or FastAPI) and Streamlit just consumes its result.
This guide covers the Hosted sign-in path
It has UseThatApp run login, so buyers return signed in and the same session answers what they bought — the Hosted sign-in add-on. If your app already has its own accounts, you do not need any of the sign-in wiring below: keep your login and verify purchases with a license key your server validates in one call, included in the per-sale fee.
The shape
- BFF server (Flask/FastAPI). Owns the OIDC flow:
/login→begin_login,/callback→complete_login,/logout→logout_url. It also exposes a tiny/meendpoint that returns the currentsuband entitlement for the browser's session cookie. This is the same Flask integration documented on the Flask page, with one extra read endpoint. - Streamlit app. Links to the BFF's
/loginand/logout, and calls/meto find out who the user is and what plan they're on. It never touches the SDK or the client secret.
Step 1 — Install
pip install usethatapp flask streamlit requestsStep 2 — The BFF read endpoint
Add /login, /callback, and /logout exactly as on the Flask page. Then add a JSON /me endpoint that reports the live entitlement for the current session, reconciling a revoked token.
from flask import session
from usethatapp import UtaTokenError, get_entitlement
@app.get("/me")
def me():
token = session.get("uta_access_token")
if not token:
return {"signed_in": False} # anonymous = your free tier
try:
ent = get_entitlement(token)
except UtaTokenError:
# Token revoked/expired (signed out of UseThatApp). Reconcile.
for k in ("uta_access_token", "uta_sub", "uta_id_token"):
session.pop(k, None)
return {"signed_in": False}
return {
"signed_in": True,
"sub": session.get("uta_sub"),
"entitled": ent.entitled,
"product_public_id": ent.product_public_id,
"version": ent.version,
"status": ent.status,
}Step 3 — Streamlit reads the BFF
Point Streamlit at the BFF. Forward the browser's cookies so the BFF can identify the session, then gate the UI on the stable product_public_id, not the mutable version display name. Link out to the BFF for sign-in and sign-out — those are full-page redirects the companion server owns.
import requests
import streamlit as st
BFF = "http://localhost:5000"
me = requests.get(f"{BFF}/me", cookies=st.context.cookies).json()
if not me.get("signed_in"):
st.markdown(f"[Log in with UseThatApp]({BFF}/login)")
st.info("You're on the free tier. Log in to see your plan.")
else:
st.markdown(f"[Log out]({BFF}/logout)")
if me["entitled"] and me["product_public_id"] == "prod_<your-pro-id>":
st.success(f"Pro features unlocked — {me['version']}")
else:
st.write(f"Plan: {me['version']} (status={me['status']})")Use sub as the user key
The BFF returns the user's pairwise sub — stable per app, uncorrelatable across apps, no PII. Key your user records off sub, never an email. The entitlement response does not include sub; it comes from the login, which the BFF holds.
Offer the upgrade: purchase links
When /me says the user is not entitled — or is on the free tier — send them to hosted checkout. UseThatApp is the merchant of record (payment, tax, refunds), and the buyer comes back signed in with the entitlement already live: no webhooks, no polling. It fits the BFF split cleanly: purchase_url() and manage_url() are pure URL builders — no network call — so the BFF hands ready-made URLs to Streamlit, which just renders them.
from usethatapp import manage_url, purchase_url
PRO_PRICE_ID = "<prc-price-id>" # from get_prices() or your dashboard
@app.get("/me")
def me():
# ... token check and entitlement read exactly as in Step 2 ...
return {
"signed_in": True,
"sub": session.get("uta_sub"),
"entitled": ent.entitled,
"product_public_id": ent.product_public_id,
"version": ent.version,
"status": ent.status,
# Pure URL builders — no network call. Streamlit just renders them.
"buy_url": purchase_url(PRO_PRICE_ID),
"manage_url": manage_url(),
}if me.get("signed_in"):
if me["entitled"] and me["product_public_id"] == "prod_<your-pro-id>":
st.link_button("Manage subscription", me["manage_url"])
else:
# Hosted checkout; the buyer returns signed in, entitled.
st.link_button("Upgrade to Pro", me["buy_url"])For a live pricing page, have the BFF render from get_prices() — never hardcode prices. Each Price carries a ready-made buy_url and the product_id that matches the entitlement’s product_public_id. See the Pricing API and Sell from your own website.
Run it
Run the BFF and Streamlit side by side. Register the BFF's /callback as the redirect URI on the UseThatApp dashboard.
# terminal 1 — the BFF
export UTA_CLIENT_ID=... UTA_CLIENT_SECRET=...
export UTA_REDIRECT_URI=http://localhost:5000/callback
# Pointing at a non-production instance? Set BOTH of these
# (each defaults to production):
# export UTA_ISSUER=http://localhost:8000/o
# export UTA_API_URL=http://localhost:8000
python bff.py
# terminal 2 — Streamlit
streamlit run streamlit_app.pyErrors
Error handling lives in the BFF, at the entitlement endpoint: 401 raises UtaTokenError (re-authenticate or refresh) and 403 raises UtaPermissionError (a valid token missing the entitlements scope). Every SDK error inherits from UtaError. See Error Handling and Gotchas.
