Async
Async is first-class, not an afterthought. Every context function has an
a-prefixed async form sharing the same pure bookkeeping, and the RLS facade
provides an async session dependency.
Setup
Give the facade an AsyncEngine (built with an async driver such as asyncpg):
from sqlalchemy.ext.asyncio import create_async_engine
from fastapi_rls import RLS
async_engine = create_async_engine("postgresql+asyncpg://app_user:…@localhost/app")
rls = RLS(async_engine=async_engine)
The facade auto-creates an async_sessionmaker (with expire_on_commit=False)
from the engine; pass your own via async_sessionmaker= if you need custom
settings.
SQLAlchemy's async layer requires greenlet. It is normally pulled in with
SQLAlchemy's async extra; if you see ValueError: the greenlet library is required, pip install greenlet.
The async session dependency
from fastapi import Depends, FastAPI
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi_rls import RLS
rls = RLS(async_engine=async_engine)
def identity(user=Depends(get_current_user)) -> dict:
return {"tenant_id": user.tenant_id}
get_session = rls.async_session_dependency(identity=identity)
app = FastAPI()
@app.get("/documents")
async def list_documents(session: AsyncSession = Depends(get_session)):
result = await session.scalars(select(Document))
return result.all()
The semantics are identical to the sync path: a per-request transaction, identity
applied with SET LOCAL, context discarded on commit.
The low-level async API
For non-FastAPI async code, the context helpers mirror the sync ones:
from fastapi_rls import (
aset_rls_context,
aapply_rls_context,
aclear_rls_context,
arls_context,
)
from fastapi_rls.adapters.sqlalchemy import AsyncSessionExecutor
executor = AsyncSessionExecutor(session)
await aapply_rls_context(executor, {"tenant_id": 42})
async with arls_context(executor, tenant_id=99):
... # scoped, restored on exit
Task isolation
The in-process context mirror lives in a ContextVar written copy-on-write, so
concurrent requests never observe each other's identity. An ASGI server runs
each request in its own copied context; interleaved requests with tenant_id 1,
2, and 3 keep three distinct identities, and nothing leaks back to the parent.
This is verified in the test suite: three concurrent requests, forced to interleave, each read back only their own identity, and the parent context stays empty.
The pool-checkin scrub backstop is installed on sync engines. The async path
relies on the SET LOCAL transaction, which is sufficient — the setting dies
with the transaction regardless of engine flavor. Native async pool hygiene is
deferred to a later release.