Stackure

Stackure Python SDK

Protect ASGI and WSGI apps, such as FastAPI, Flask and Django. Zero dependencies.

The stackure package adds Stackure sign-in to a Python app with one line of middleware. For how sign-in works in every SDK, see the overview. To call the API without an SDK, see the API reference.

Install

pip install stackure

It needs Python 3.14 or later. The package is stackure on PyPI.

Configure

export STACKURE_APP_ID=...       # the app's ID, shown on the app's page in Stackure
export STACKURE_APP_SECRET=...   # shown once, when you register the app or rotate its secret
export STACKURE_BASE_URL=https://stackure.com

Until you share a new app, only your organization's app admins and owner can sign in to it. Share it with everyone or with chosen teams under Available To on the app's page.

Protect an app

import stackure

# ASGI: FastAPI, Starlette, Quart
app = stackure.auth()(app)

# WSGI: Flask
flask_app.wsgi_app = stackure.auth()(flask_app.wsgi_app)

# WSGI: Django, in wsgi.py
application = stackure.auth()(get_wsgi_application())

The same wrapper handles both. It tells ASGI from WSGI by the app you wrap. Requests that aren't signed in never reach your app:

The JSON 401 looks like this:

{"error": "Unauthorized", "message": "Valid authentication required", "sign_in_url": "https://stackure.com/sign-in/magic-link?app_id=..."}

The signed-in user

user = stackure.user_from_request(request)
print(user.user_email, user.account_id)

user_from_request takes a framework request, a WSGI environ or an ASGI scope. It returns the User that auth or mcp attached. Without one, it returns None.

FieldValue
user_idThe user's ID
account_idThe ID of the user's organization
user_emailEmail address
user_first_nameFirst name
user_last_nameLast name
user_is_app_adminThe user is an app admin or the owner in their organization, in charge of its apps
user_teamsThe teams the user belongs to, a tuple of Team(team_id, team_name), empty when none

Stackure defines no permissions inside your app. Your app decides what these facts mean.

List who can open the app

List the users in the caller's organization who can open the app, and the teams that have it, for sharing and assigning inside your app. Each call returns one page, sorted by name:

page = stackure.directory_users(request, query="ada")
# page.items: DirectoryUser(user_id, user_email, user_first_name, user_last_name)
# page.next_cursor, page.total

teams = stackure.directory_teams(request, limit=50)
# teams.items: Team(team_id, team_name)

Sign-in handoff

You don't write this part. For what the user sees, see the overview. After they sign in:

  1. Stackure posts a session_token form field to your app's registered URL.
  2. The middleware checks the token with Stackure, stores it in a stackure_session cookie on your domain and redirects to the same URL as a GET.

Sessions

Protect an MCP endpoint

AI clients such as Claude, Claude Code, VS Code and Cursor sign users in through Stackure and send the credential it issues as Authorization: Bearer. Wrap your MCP app with mcp and route to it beside the rest of your app, not behind auth:

from starlette.applications import Starlette
from starlette.routing import Mount

app = Starlette(routes=[
    Mount("/mcp", stackure.mcp()(mcp_app)),
    Mount("/", stackure.auth()(web_app)),
])

For WSGI, for example with Flask:

from werkzeug.middleware.dispatcher import DispatcherMiddleware

flask_app.wsgi_app = DispatcherMiddleware(
    stackure.auth()(flask_app.wsgi_app),
    {"/mcp": stackure.mcp()(mcp_wsgi_app)},
)

Verify without middleware

result = stackure.verify(request)

if not result.authenticated:
    # result.error.code, result.error.message, result.error.sign_in_url
    ...

# result.user
resp = stackure.send_magic_link("user@example.com")
# resp.message

Sign out

METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]

# Flask
@app.route("/logout", methods=METHODS)
def logout():
    r = stackure.logout(request)
    return "", r.status, r.headers

# FastAPI
@app.api_route("/logout", methods=METHODS, include_in_schema=False)
async def logout(request: Request):
    r = await asyncio.to_thread(stackure.logout, request)
    return Response(status_code=r.status, headers=dict(r.headers))

# Starlette: the same function, routed with
Route("/logout", logout, methods=METHODS)
<form method="post" action="/logout"><button>Sign Out</button></form>

Errors

Every function except verify and logout raises StackureError. Those two never raise. Branch on .code:

from stackure import StackureError

try:
    stackure.send_magic_link(email)
except StackureError as err:
    match err.code:
        case "validation": ...
        case "auth": ...
        case "forbidden": ...
        case "timeout": ...
        case "network": ...
CodeWhen
validationBad input or a missing or malformed setting, caught before any call
auth401 from the API, such as for a wrong app secret
forbidden403 from the API
timeoutThe 2-second deadline passed
networkEverything else, including 429 and 5xx answers

err.message says what happened. err.status_code is the HTTP status. It's None when no answer came back.

Timeouts and limits

More