Security model
fastapi-rls is a security library, so its threat model is explicit. Read this page before you rely on it in production.
The non-superuser requirement (read this twice)
PostgreSQL superusers and roles with BYPASSRLS ignore Row Level Security
entirely. If your application connects as such a role, every policy silently
becomes a no-op and you leak everything — while all your tests that use the same
role pass.
-- Your application role must be an ordinary role:
CREATE ROLE app_user LOGIN PASSWORD '…';
-- NOT a superuser, and NOT created with BYPASSRLS.
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO app_user;
The fastapi-rls integration test suite deliberately connects as a dedicated non-superuser role for exactly this reason — a superuser connection would make cross-tenant leak tests pass even if RLS were broken.
FORCE ROW LEVEL SECURITY is on by default
By default, RLS does not apply to the table's owner. fastapi-rls issues
ALTER TABLE … FORCE ROW LEVEL SECURITY so the owner is constrained too. This
matters because migrations and app connections often run as the owner. You can
opt out per-operation (force=False), but the safe default is forced.
The injection trust boundary
SQL identifiers (table, column, role, operation names) are positions PostgreSQL
cannot parameterize — they go into the DDL as text. fastapi-rls treats a
single module (fastapi_rls.sql) as the trust boundary:
- Table/column/policy/key identifiers are validated against a strict allowlist regex and quoted.
- Roles must be
PUBLICor a comma-separated list of valid identifiers. - Operations must be one of
ALL/SELECT/INSERT/UPDATE/DELETE. CustomPolicyexpressions are screened for statement terminators, comments, and DML/DDL keywords.
Validation happens both at policy construction and again in the DDL layer — the last line of defense before a string reaches the database.
The screen blocks obvious payloads; it is not a SQL parser. If you interpolate a
value into a CustomPolicy expression, that is on you — construct it safely, or
use TenantPolicy/UserPolicy, which never interpolate free text.
What identity values are trusted
fastapi-rls propagates the identity you resolve; it does not authenticate.
The security of the boundary is only as good as your identity callable. If that
function derives tenant_id from an unverified request header, an attacker sets
their own tenant. Resolve identity from a verified source (a validated JWT
claim, a server-side session), the same way you would for any authorization
decision.
Absent context fails closed
Because policies read NULLIF(current_setting('rls.tenant_id', true), ''), a
request with no identity set produces a NULL comparison — the predicate is
false and no rows are returned. Missing context can never accidentally mean
"see everything". Set require_context=True to turn a missing context into a
loud RLSContextRequiredError instead of a silent empty result.
Threats fastapi-rls does not address
- Authentication. You own it.
- Authorization beyond row visibility. Endpoint-level access control is yours.
- A compromised or misconfigured superuser connection. RLS cannot constrain a role that bypasses RLS. Guard your credentials and role grants.
- Application bugs that leak data before it reaches the database (e.g. caching another tenant's response). RLS protects the query, not your cache.
Production checklist
See the dedicated production checklist for the full list, but the essentials:
- App connects as a non-superuser, non-
BYPASSRLSrole. - RLS is enabled and forced on every protected table (
fastapi-rls audit). identityderives context from a verified principal.- Consider
require_context=Trueso a missing context fails loud.