Introduction
fastapi-rls brings PostgreSQL Row Level Security (RLS) to FastAPI and
SQLAlchemy applications. It lets you enforce multi-tenant and per-user data
isolation at the database level, where it cannot be bypassed by a forgotten
WHERE clause.
It is 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.
Why RLS instead of WHERE tenant_id = ?
Application-level filtering is a promise you have to keep on every single query, forever, across every developer who ever touches the codebase:
# One of these forgets the tenant filter. Which one? You'll find out in an incident.
db.query(Invoice).filter(Invoice.tenant_id == tenant_id).all()
db.query(Invoice).filter(Invoice.status == "overdue").all() # 🔴 leaks every tenant
Row Level Security moves that guarantee into PostgreSQL. Once a policy is in place, the database itself refuses to return rows the current context is not allowed to see — for reads and writes, for the ORM and raw SQL, for the report you wrote today and the endpoint someone adds next year.
# With a TenantPolicy in place, this returns only the caller's rows.
# PostgreSQL applies the predicate; there is no WHERE clause to forget.
session.scalars(select(Invoice).where(Invoice.status == "overdue")).all()
How it works, in one paragraph
You declare policies on your tables (TenantPolicy, UserPolicy, or a raw
CustomPolicy). Each compiles to a PostgreSQL predicate that reads the current
identity from a session variable — for example
tenant_id = current_setting('rls.tenant_id'). On each request, the FastAPI
session dependency opens a transaction and runs SET LOCAL rls.tenant_id = …
from the identity you resolved. PostgreSQL enforces the predicate on every
statement. When the transaction ends, the setting is gone — and a pool-checkout
scrub is a second line of defense so identity can never survive into another
request.
What fastapi-rls does and does not do
It does:
- Compile policy objects to native PostgreSQL RLS DDL.
- Propagate a request's identity into the database connection safely.
- Reconcile your declared policies with the live database (
sync/plan/audit). - Work with sync and async SQLAlchemy, with or without Alembic.
It does not:
- Authenticate. You resolve the principal (from a JWT, session, header, whatever); fastapi-rls propagates it. It never decides who the caller is.
- Replace authorization logic that isn't row visibility (e.g. "can this user hit this endpoint at all").
- Work against a superuser connection. PostgreSQL superusers and
BYPASSRLSroles ignore RLS entirely — see the security model.
Next steps
- Installation — pick the right extras.
- Quick start — a working multi-tenant endpoint in a few minutes.
- Security model — the non-superuser requirement and the trust boundary. Read this before production.