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.

Written and maintained by · source on GitHub