🚀 New: chi (χ) — an open-source autoresearch harness for fleets of LLM coding agents. Read the announcement.

fastapi-rls

Row-level security for FastAPI & SQLAlchemy — sync and async, leak-proof by construction, enforced by PostgreSQL.

Stack
FastAPI · SQLAlchemy · PostgreSQL
Status
active
Source
https://github.com/kdpisda/fastapi-rls
Website
https://fastapi-rls.com

The FastAPI/SQLAlchemy sibling of django-rls: the policy model, the session-variable context, and the generated DDL are intentionally the same; only the framework integration differs. The core is ORM-agnostic — policies, context, and DDL have zero framework dependencies; SQLAlchemy, FastAPI, and Alembic are optional adapters.

How it works

Models declare policies through a mixin, and a session dependency scopes every query to the caller’s identity:

class Document(Base, RLSMixin):
    __tablename__ = "documents"
    __rls_policies__ = [
        TenantPolicy("tenant_isolation", column="tenant_id"),
    ]
    id: Mapped[int] = mapped_column(primary_key=True)
    tenant_id: Mapped[int] = mapped_column(index=True)

def identity(user = Depends(get_current_user)) -> dict:
    return {"tenant_id": user.tenant_id}   # you authenticate, it propagates

get_session = rls.session_dependency(identity=identity)

@app.get("/documents")
def list_documents(session: Session = Depends(get_session)):
    return session.scalars(select(Document)).all()   # Postgres filters by tenant

Async is identical — rls.async_session_dependency with an AsyncSession. A request with no context sees no rows, never everyone’s. Applying policies works three ways: rls.sync() at startup, Alembic operation directives, or the standalone CLI (fastapi-rls sync / plan / audit), which also reports drift between declared policies and what’s live in the database.

Under the hood

  • Policies compile to predicates like tenant_id = (SELECT NULLIF(current_setting('rls.tenant_id', true), '')::integer) — the current_setting read wrapped in a scalar subquery so the planner evaluates it once per statement, not per row.
  • Identity is applied with SET LOCAL inside a per-request transaction; commit clears it, and a pool-checkout scrub is the defense-in-depth backstop. user_id/tenant_id cannot be silently overridden mid-request.
  • TenantPolicy, UserPolicy, and injection-screened CustomPolicy cover the policy space, with per-operation scoping, RESTRICTIVE policies, role targeting, and integer/bigint/uuid/text context types.
  • Table, column, role, and operation names are validated against a strict identifier allowlist before any DDL; FORCE ROW LEVEL SECURITY is applied by default.
  • Tested against real PostgreSQL — cross-tenant leak tests included — across Python 3.10–3.13 and PostgreSQL 13–17.

Published on PyPI as fastapi-rls (BSD-3-Clause), with docs at fastapi-rls.com. Full story in the deep dive.