Skip to main content

SQLAlchemy models

The SQLAlchemy adapter is the sugar layer over the ORM-agnostic core. Its job is to derive table names, register policies, and run the set_config round-trip on a session.

Declaring policies with RLSMixin​

Mix RLSMixin into a model and list its policies. On class creation the mixin binds each policy to __tablename__ and registers it into the default registry.

from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from fastapi_rls import TenantPolicy, UserPolicy
from fastapi_rls.adapters.sqlalchemy import RLSMixin


class Base(DeclarativeBase):
pass


class Document(Base, RLSMixin):
__tablename__ = "documents"
__rls_policies__ = [
TenantPolicy("tenant_isolation", column="tenant_id"),
UserPolicy("owner_can_write", column="owner_id", operation="UPDATE"),
]

id: Mapped[int] = mapped_column(primary_key=True)
tenant_id: Mapped[int] = mapped_column(index=True)
owner_id: Mapped[int] = mapped_column(index=True)
title: Mapped[str]

Because the policies are bound to the table by the mixin, you do not pass table= here.

Using a specific registry​

By default policies register into the global default_registry. To keep a model's policies in a separate registry, set __rls_registry__ on the class:

from fastapi_rls import PolicyRegistry

reporting_registry = PolicyRegistry()


class Report(Base, RLSMixin):
__tablename__ = "reports"
__rls_registry__ = reporting_registry
__rls_policies__ = [TenantPolicy("tenant_isolation", column="tenant_id")]

Then point the facade at it: RLS(engine=engine, registry=reporting_registry).

collect_policies — the version-robust path​

Some custom declarative bases don't propagate __init_subclass__ the way the mixin relies on. If your models aren't registering, call collect_policies once at startup to walk the mapper registry and register every model's policies explicitly:

from fastapi_rls.adapters.sqlalchemy import collect_policies

collect_policies(Base) # into default_registry
collect_policies(Base, reporting_registry) # into a specific registry

This is idempotent and safe to call at import time.

Executors​

The core context API talks to the database through an RLSExecutor protocol. The adapter provides two implementations that wrap a session:

  • SessionExecutor(session) — runs SELECT set_config(:key, :value, :is_local) and SELECT current_setting(:key, true) on a sync Session.
  • AsyncSessionExecutor(session) — the async equivalent for AsyncSession.

You rarely construct these yourself — the FastAPI dependencies do it for you — but they're public for advanced or non-FastAPI use.

Pool hygiene​

install_pool_hygiene(engine) registers a checkin listener that scrubs rls.* session variables (or drops the connection) when it returns to the pool. The RLS facade installs it automatically for a sync engine when reset_context_on_checkout is enabled. It is a defense-in-depth backstop; the primary guarantee is the SET LOCAL transaction (see request context).

Applying policies to the database​

Once models are registered, reconcile with the database via the facade:

from fastapi_rls import RLS

rls = RLS(engine=engine)
rls.sync() # ENABLE + FORCE RLS and CREATE every registered policy

Or via the CLI / Alembic. Next: wire it into FastAPI requests.

Written and maintained by · source on GitHub