Skip to main content

flask-rls

License Python SQLAlchemy

PostgreSQL Row-Level Security for Flask and SQLAlchemy.

Security enforced by PostgreSQL, not by application code. Once a table has RLS enabled and a tenant/user policy attached, the database itself returns only the rows the current request is authorized to see — even for raw SQL or a forgotten .filter().

flask-rls is the Flask/SQLAlchemy sibling of django-rls: the same concepts (TenantPolicy, UserPolicy, CustomPolicy, Pythonic policies, RLS.user_id()), adapted to SQLAlchemy Core and the Flask request lifecycle.

Features

  • 🔒 Database-level Row-Level Security using PostgreSQL RLS
  • 🏢 Tenant-based and user-based policies
  • 🐍 Pythonic policies — compose predicates from SQLAlchemy Core expressions
  • ⚡ Pool-safe — context is set per transaction via set_config(..., is_local=true), so it cannot leak across pooled connections
  • 🧩 ORM-agnostic — works with bare SQLAlchemy or Flask-SQLAlchemy
  • 🧱 Alembic migration operations (op.enable_rls, op.create_policy, …)
  • 🛠️ flask rls sql to dump policy DDL

Installation

pip install flask-rls            # core
pip install "flask-rls[alembic]" # + Alembic migration operations

Requires Python 3.10+, SQLAlchemy 2.0+, Flask 2.2+, and PostgreSQL 12+.

Quick start

1. Initialize the extension

from flask import Flask, g
from flask_sqlalchemy import SQLAlchemy
from flask_rls import RLS

db = SQLAlchemy()
rls = RLS()

def create_app():
    app = Flask(__name__)
    app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql+psycopg://app@localhost/mydb"
    db.init_app(app)
    with app.app_context():
        rls.init_app(app, engine=db.engine)

    @app.before_request
    def set_tenant():
        g.tenant_id = current_tenant_id()  # however your app resolves it
        g.user_id = current_user_id()

    return app

flask-rls reads g.tenant_id / g.user_id at each transaction begin and issues SELECT set_config('rls.tenant_id', ..., true). No context set → the GUC is NULL → policies match zero rows (fail closed).

2. Define and register policies

from flask_rls import TenantPolicy, UserPolicy, ExpressionPolicy, RLS
from sqlalchemy import column, true

rls.register("invoices", TenantPolicy("tenant_isolation", "tenant_id"))

# Pythonic policy: owner OR public
rls.register(
    "projects",
    ExpressionPolicy(
        "project_access",
        expr=(column("owner_id") == RLS.user_id()) | (column("is_public") == true()),
    ),
)

rls.register(...) feeds the flask rls sql dumper. Apply the DDL through Alembic (below) or by piping flask rls sql into a migration.

3. Apply via Alembic

# migrations/env.py
from flask_rls.alembic import *  # noqa: F401,F403  registers op.enable_rls, ...

# a migration
from flask_rls import TenantPolicy

def upgrade():
    op.enable_rls("invoices")
    op.force_rls("invoices")
    op.create_policy("invoices", TenantPolicy("tenant_isolation", "tenant_id"))

def downgrade():
    op.drop_policy("invoices", "tenant_isolation")
    op.disable_rls("invoices")

Running privileged / background work

Outside a request there is no g, so context falls through to fail-closed. Use the scope managers:

with rls.override(tenant_id=42):   # privileged identity switch (jobs, CLI, tests)
    generate_monthly_report()

with rls.bypass():                 # emit no context — for non-RLS tables
    create_new_tenant()

bypass() is not god-mode: on an RLS table it still sees zero rows. True cross-tenant access requires a PostgreSQL role with BYPASSRLS (see the design doc).

Important: the owner-bypass gotcha

A table's owner (and superusers) bypass RLS entirely unless the table has FORCE ROW LEVEL SECURITY. flask-rls emits FORCE for every registered table, and your application should connect as a non-owner role regardless.

Configuration

Flask config key Default Meaning
RLS_GUC_PREFIX rls. GUC namespace prefix
RLS_G_TENANT_KEY tenant_id flask.g attribute read for the tenant
RLS_G_USER_KEY user_id flask.g attribute read for the user
RLS_REQUIRE_CONTEXT False raise RLSContextRequiredError when context is missing
RLS_DEBUG False debug-log each set_config call

License

BSD 3-Clause — see LICENSE.

Metadata

Release files for flask-rls 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flask-rls 0.1.1
File Size Uploaded Provenance Actions
flask_rls-0.1.1.tar.gz 296.2 kB With provenance Details for flask_rls-0.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for flask-rls 0.1.1
File Interpreter ABI Platform Actions
flask_rls-0.1.1-py3-none-any.whl Python 3 none any Details for flask_rls-0.1.1-py3-none-any.whl

Total release size: 314.1 kB

Release files / flask_rls-0.1.1.tar.gz

Download URL flask_rls-0.1.1.tar.gz
Size 296.2 kB
Tags Source
SHA-256 checksum
How to use checksums
f2d20a820c919a33f1c933b34d5d73b6274daae10810d8c981a541f9e5a71c09
BLAKE2b-256 checksum
How to use checksums
abf3496fc63e7e932dccb76ec1cfed230d9cc90e4915dff1e053934ae82f386c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.

Transparency log

Release files / flask_rls-0.1.1-py3-none-any.whl

Download URL flask_rls-0.1.1-py3-none-any.whl
Size 17.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8436d9cca427babc95a222fd9fa94372a82d1f74934e0c88f8d2c8af6eb56d36
BLAKE2b-256 checksum
How to use checksums
12ef844ca80a7eb372c0b3384c962b96879e99516c0f5d6eb8dea024bfef5b9a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.

Transparency log

Security for flask-rls 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for flask_rls-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page