Troubleshooting
Every RLS problem shows up as one of three symptoms. Find yours, run the checks, and you'll usually have the cause in under five minutes.
Symptom 1: I can see rows from other tenants
This is the dangerous one. Work through these in order — the first check finds the culprit in the vast majority of cases.
Check the role (the #1 cause)
PostgreSQL superusers and roles with BYPASSRLS skip RLS entirely — every
policy silently becomes a no-op. Check the role your app actually connects
as:
SELECT rolname, rolsuper, rolbypassrls
FROM pg_roles
WHERE rolname = current_user;
-- rolsuper and rolbypassrls must BOTH be false
Run this through your application's own connection (a scratch endpoint or a
one-off script using the same DATABASE_URL), not through psql as postgres —
the whole point is verifying what the app uses.
Check the policies actually exist
Policies declared in Python do nothing until they're applied to the database:
fastapi-rls audit # compares the registry against pg_policies
or in SQL:
SELECT tablename, policyname, qual FROM pg_policies;
SELECT relname, relrowsecurity, relforcerowsecurity
FROM pg_class WHERE relname = 'documents';
-- relrowsecurity AND relforcerowsecurity should be true
If the table is missing, run rls.sync() at startup, fastapi-rls sync from
the CLI, or apply the Alembic migration that creates the policies.
Check FORCE is on
ENABLE ROW LEVEL SECURITY alone does not constrain the table's owner —
and apps are often connected as the owner in development. fastapi-rls issues
FORCE ROW LEVEL SECURITY by default; if relforcerowsecurity is false in
the query above, something disabled it — re-run fastapi-rls sync.
Check you're testing as the right role
A leak "reproduced" in psql as postgres isn't a leak — it's the superuser
bypass above. The integration-test advice in the
production checklist exists for exactly this reason.
Symptom 2: Queries return no rows (but the data exists)
This is RLS working as designed: a query with no identity context matches nothing. fastapi-rls fails closed — no context means no rows, never all rows. The question is why the context is missing:
The session didn't come from the dependency
Identity is applied per-request by the session dependency
(rls.session_dependency(identity=...) / rls.async_session_dependency).
A session or connection created any other way — a module-level
sessionmaker, a background job, a shell script — carries no identity, so it
sees no rows. Give background work an explicit context:
from fastapi_rls import rls_context
with rls_context(executor, tenant_id=42):
... # queries here see tenant 42's rows
The identity key doesn't match the policy
A TenantPolicy(column="tenant_id") reads current_setting('rls.tenant_id').
If your identity function returns {"org_id": ...}, the setting the policy
reads is never populated. Compare your identity dict's keys against the
policy's expected key, and inspect the generated SQL with:
fastapi-rls plan
The context was cleared by the transaction boundary
SET LOCAL lives exactly as long as the transaction. If you commit() and
then keep querying on the same session, the second batch of queries runs
without identity. Keep a request's work inside the dependency-managed
transaction, or set the context again after committing.
You expected the contextvar to follow you into a sync handler
FastAPI runs sync dependencies and sync route handlers in different threadpool
contexts, so get_active_rls_context() inside a sync handler may be empty —
enforcement is unaffected (it rides the session's SET LOCAL), but don't
use the contextvar mirror for display logic in sync handlers; read identity
from your own dependency instead.
Symptom 3: Exceptions
RLSContextRequiredError
Raised when identity context is required but missing — typically from
require_rls_context() or a dependency configured to demand identity. Fix:
ensure the identity function returns a non-empty dict for authenticated
requests, and give non-request code an explicit rls_context(...).
RLSContextImmutableError
user_id and tenant_id are immutable once set in a scope — a second
set_rls_context(tenant_id=...) with a different value raises instead of
silently switching tenants mid-request. That's deliberate: a request that
changes identity halfway through is almost always a bug or an attack. If you
genuinely need elevation (admin tooling, cross-tenant maintenance), make it
explicit and auditable:
from fastapi_rls import system_rls_context
with system_rls_context(executor):
... # privileged, logged scope
TenantAccessDeniedError
The tenant context failed membership validation — the authenticated user isn't a member of the tenant they asked for. Check the membership data before suspecting the library.
Still stuck?
fastapi-rls audittells you how the database differs from your declared policies;fastapi-rls planshows the exact SQL it would run.- The security model defines precisely what fastapi-rls does and does not protect against.
- Found a leak you can reproduce with a non-superuser role? Report it privately — see the security policy.
Written and maintained by Kuldeep Pisda · source on GitHub