Skip to main content

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.

greenlet

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.

Async pool hygiene

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.