qelos-integrator-fastapi
FastAPI / Starlette middleware that resolves the current Qelos user, active workspace, and a per-request SDK client before your handlers run, and exposes them on request.state.qelos.
This is the Python implementation of the Qelos integrator contract — the same shape exposed by @qelos/integrator-express, @qelos/integrator-fastify, @qelos/integrator-nest, @qelos/integrator-next, and @qelos/integrator-nuxt.
If you are new to Qelos, read Getting Started as an Integrator first for the overall flow (CLI, blueprints, deployment).
1. Install
pip install qelos-integrator-fastapi qelos-sdkRequirements:
- Python 3.9+
- FastAPI / Starlette. The middleware is ASGI-level, so any Starlette app works too.
2. Configure the middleware
from fastapi import FastAPI
from qelos_integrator_fastapi import QelosConfig, QelosIntegratorMiddleware
app = FastAPI()
app.add_middleware(
QelosIntegratorMiddleware,
config=QelosConfig(app_url="https://your-qelos-instance.com"),
)The middleware:
- Reads the access token from
Authorization: Bearer …orq_access_token, and the refresh token fromq_refresh_token. - Builds a per-request
QelosSDKinstance bound to those tokens (create_request_sdk). - Calls
authentication.get_logged_in_user()andworkspaces.get_list(). - Picks the active workspace (first by default, or your
resolve_workspacecallable). - Attaches everything to
request.state.qelos.
All configuration options
QelosConfig(
app_url="https://your-qelos-instance.com", # required
# Service-to-service: skip cookies/refresh entirely.
api_token="...",
# Cookie names. Defaults shown.
access_token_cookie="q_access_token",
refresh_token_cookie="q_refresh_token",
# Reject anonymous requests with 401. Defaults to False.
require_auth=False,
# Skip the middleware entirely for these path prefixes.
skip_paths=["/health", "/metrics"],
# Anything you want passed through to the per-request SDK.
sdk_options={},
)You can also pass resolve_workspace= and on_token_refresh= to add_middleware:
async def resolve_workspace(request, user, workspaces):
target_id = request.headers.get("x-qelos-workspace")
return next((w for w in workspaces if w["_id"] == target_id), workspaces[0] if workspaces else None)
app.add_middleware(
QelosIntegratorMiddleware,
config=QelosConfig(app_url="https://your-qelos-instance.com"),
resolve_workspace=resolve_workspace,
)3. Access user and workspace in your routes
The context is a QelosRequestContext:
@dataclass
class QelosRequestContext:
sdk: QelosSDK # bound to this request's tokens
tokens: TokenPair # mutated in place when refreshed
user: Optional[Dict[str, Any]] # None for anonymous requests
workspace: Optional[Dict[str, Any]]
workspaces: List[Dict[str, Any]]User and workspace come back as plain dicts matching the JSON returned by the Qelos API.
Inject it via the get_qelos and require_user dependencies:
from typing import Annotated, Optional
from fastapi import Depends, FastAPI
from qelos_integrator_fastapi import QelosRequestContext, get_qelos, require_user
@app.get("/me")
async def me(qelos: Annotated[Optional[QelosRequestContext], Depends(get_qelos)]):
return {
"user": qelos.user if qelos else None,
"workspace": qelos.workspace if qelos else None,
}
@app.get("/private")
async def private(qelos: Annotated[QelosRequestContext, Depends(require_user)]):
# 401 returned automatically when there's no user.
return qelos.userYou can also read it straight off request.state.qelos:
from starlette.requests import Request
@app.get("/me-raw")
async def me_raw(request: Request):
qelos = getattr(request.state, "qelos", None)
return {"user": qelos.user if qelos else None}require_user raises HTTPException(status_code=401, detail={"code": "UNAUTHORIZED"}) when no user is attached — equivalent to config.require_auth=True but scoped per-route.
4. Handle authentication
The integrator only resolves identity from existing tokens; it does not host the login UI.
Cookie-based session (recommended for browsers)
Most flows let users sign in directly against the Qelos backend (admin panel, hosted login page, or a frontend that calls sdk.authentication.signin). Qelos sets q_access_token and q_refresh_token cookies on the user's browser; the middleware reads them on subsequent requests.
You can also drive the login from a FastAPI route. The qelos-sdk package returns the Set-Cookie header so you can forward it:
from fastapi import FastAPI, Response
from qelos_sdk import QelosSDK
@app.post("/auth/login")
async def login(body: dict, response: Response):
sdk = QelosSDK(app_url="https://your-qelos-instance.com")
result = await sdk.authentication.signin(body)
set_cookie = result.get("headers", {}).get("set-cookie")
if set_cookie:
response.headers.append("set-cookie", set_cookie)
return {"user": result["payload"]["user"]}Social login
from fastapi.responses import RedirectResponse
@app.get("/auth/google")
async def google():
sdk = QelosSDK(app_url="https://your-qelos-instance.com")
url = sdk.authentication.get_social_login_url(
"google",
return_url="https://your-app.com/dashboard",
)
return RedirectResponse(url)
@app.get("/auth/callback")
async def callback(rt: str):
sdk = QelosSDK(app_url="https://your-qelos-instance.com")
result = await sdk.authentication.exchange_auth_callback(rt)
response = RedirectResponse("/")
set_cookie = result.get("headers", {}).get("set-cookie")
if set_cookie:
response.headers.append("set-cookie", set_cookie)
return responseToken refresh
When tokens rotate, the optional on_token_refresh hook runs. The default implementation schedules Set-Cookie headers on the outgoing response (HTTP-only, SameSite=Lax, Secure when app_url is HTTPS).
Because Starlette does not expose the final Response until after inner middleware runs, the default hook records cookies on an internal CookieBuffer that is flushed onto the real response when the request completes. Custom hooks receive the same buffer as TokenRefreshContext.response; call set_cookie on it if you write cookies from the hook.
from qelos_integrator_fastapi import TokenRefreshContext
async def my_refresh_hook(ctx: TokenRefreshContext) -> None:
# ctx.response is a CookieBuffer until the real response exists.
ctx.response.set_cookie(
"q_access_token",
ctx.new_tokens.access_token,
httponly=True,
samesite="lax",
)
if ctx.new_tokens.refresh_token:
ctx.response.set_cookie(
"q_refresh_token",
ctx.new_tokens.refresh_token,
httponly=True,
samesite="lax",
)
app.add_middleware(
QelosIntegratorMiddleware,
config=QelosConfig(app_url="https://your-qelos-instance.com"),
on_token_refresh=my_refresh_hook,
)Anonymous requests leave user / workspace as None unless require_auth=True, which responds with 401 and body {"code": "UNAUTHORIZED"}.
Service-to-service (no end user)
Set api_token to skip cookies and refresh entirely:
QelosConfig(
app_url="https://your-qelos-instance.com",
api_token=os.environ["QELOS_API_TOKEN"],
)5. Query entities
The SDK on qelos.sdk is already authenticated as the current user, so blueprint permissions are enforced for free:
@app.get("/products")
async def list_products(qelos: Annotated[QelosRequestContext, Depends(require_user)]):
return await qelos.sdk.entities("products").get_list({"status": "active"})
@app.post("/products", status_code=201)
async def create_product(
body: dict,
qelos: Annotated[QelosRequestContext, Depends(require_user)],
):
return await qelos.sdk.entities("products").create(body)The full surface — get_list, create, update, remove, etc. — is in the Blueprints Operations reference (the SDK shape mirrors the TypeScript SDK; method names are snake_case in Python).
6. Common patterns and gotchas
- The integrator package is for external apps only. Apps inside the Qelos monorepo MUST NOT depend on
qelos-integrator-fastapi— they talk to the gateway directly. request.state.qelosisNoneonskip_paths. Always go throughDepends(get_qelos)(which returnsOptional[...]) orDepends(require_user)(which raises401) instead of accessing the attribute blindly.- Anonymous requests don't raise by default.
qelos.userandqelos.workspacewill simply beNone. Switch torequire_auth=Trueor useDepends(require_user)when you want a hard401. - Cookie writes are buffered. A
CookieBuffercollectsSet-Cookieoperations during the request; the real response receives them once Starlette resolves it. If you write a customon_token_refresh, usectx.response.set_cookie(...)rather than trying to grab the underlyingResponsedirectly. Securecookies are based onapp_url. Localhttp://instances get cookies withoutSecureso browsers accept them; productionhttps://instances getSecureautomatically.- Workspace selection defaults to the first workspace. Supply
resolve_workspace=if your users belong to multiple workspaces. sdk_optionsis normalized. Camel-case keys from a Node-style config (e.g.accessToken) are mapped to the snake_case fields the Python SDK expects, so you can pass the same shape your other integrator services use.- Don't reuse the per-request SDK across requests. It is bound to a specific token pair. Build a fresh
QelosSDK(api_token=...)for background workers and scripts.
