Skip to main content

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.

fastapi-rls request lifecycle: identity is applied with SET LOCAL inside a per-request transaction, PostgreSQL filters every query, the transaction end clears the identity, and a pool-checkout scrub provides defense in depth

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 audit tells you how the database differs from your declared policies; fastapi-rls plan shows 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 · source on GitHub