rlsgrid
Catch cross-tenant Row-Level Security leaks in Postgres and Supabase before your users do.
What it does
Point it at your database. rlsgrid reads the live schema and:
- Maps every
role × table × operationand labels itallow/deny/conditional/unrestricted. - Fuzzes for real cross-tenant leaks — it seeds synthetic tenants and actively tries to read, insert, update, and delete one tenant's rows from another tenant's session.
- Emits a pgTAP suite you can run in CI.
Why
Postgres RLS is powerful and easy to get subtly wrong: a missing
WITH CHECK, a FOR ALL where you meant FOR SELECT, a forgotten
ENABLE ROW LEVEL SECURITY, a service_role bypass leaking client-side.
Your application unit tests will not catch any of these — they test your code,
not the policies. rlsgrid tests the policies, against a real database.
Use it
sudo apt install python3-venv -y
python3 -m venv ~/venvs/rlsgrid-env
source ~/venvs/rlsgrid-env/bin/activate
pip install rlsgrid
# if ipv6
export DATABASE_URL=postgresql://user:pw@host/db # use staging, never prod
# if ipv4
getent ahosts db-host | grep STREAM | grep 4
export DATABASE_URL=postgresql://user:pw@<response>/db
rlsgrid init --from-db # read the schema, write an annotated config
rlsgrid check --tenants 5 # seed → fuzz → teardown. exit 1 on any leak.
check is the whole loop: it leaves nothing behind and returns non-zero on a
breach, so it drops straight into CI. A leak looks like:
✗ 1 cross-tenant breach detected
LEAK role=authenticated actor_tenant=a1b2 → target_tenant=c3d4
on public.documents UPDATE: target-owned row visible across tenants
In CI (GitHub Action)
- uses: matte97p/rlsgrid@v1
with:
command: fuzz # seeds synthetic tenants, probes isolation, fails the build on any leak
database-url: ${{ secrets.STAGING_DB_URL }}
Lower-level commands
rlsgrid introspect # tables, RLS state, policies
rlsgrid plan --explain # the full matrix, with a "why" column
rlsgrid gen pgtap --out tests/rls/generated.sql # emit a pgTAP suite
rlsgrid fuzz --tenants 5 # fuzz only (auto-cleans up)
rlsgrid seed --dry-run # show the seed plan without writing
rlsgrid check --sarif-out rls.sarif # SARIF for GitHub code scanning
From pytest
Installing rlsgrid registers a rlsgrid fixture, so you can gate your
existing suite:
def test_no_cross_tenant_leaks(rlsgrid):
report = rlsgrid.check()
assert report.ok, [b.detail for b in report.breaches]
Point it with --rlsgrid-config path/to/rlsgrid.toml.
Config for your stack — Supabase, Prisma, Drizzle, SQLAlchemy, Rails, function-based access checks — is in docs/RECIPES.md.
How it classifies a cell
- allow — a permissive policy applies and gates nothing.
- deny — RLS is on and no policy matches the role/op.
- conditional — a policy applies but a
USING/WITH CHECKexpression gates which rows. This is where the fuzz earns its keep. - unrestricted — RLS is off, or the role has
BYPASSRLS. Surfaced explicitly so you notice when you did not mean it.
Two enforcement models
- RLS at the database (the Supabase default): the fuzz finds leaks
directly. Set
tenancy.mode = "jwt". - Access enforced by a SQL function (e.g.
check_user_has_access_to_store(user_id, store_id)): settenancy.mode = "function"and rlsgrid calls the helper with cross-tenant arguments, asserting it returns false.
How it compares
| hand-written pgTAP | static linters | rlsgrid | |
|---|---|---|---|
| New table lands without a test | silent | maybe | shows up in plan |
| Cross-tenant write leaks | only if you wrote that test | no | probed automatically |
| Function-based access | no | no | first-class |
| Setup | per-test | low | one config |
It composes with supabase-test-helpers:
keep your bespoke business-rule pgTAP, let rlsgrid watch the floor.
Safety
seed, fuzz, and check write to the database, so they refuse any URL
matching [safety].forbid_url_patterns (default ["prod", "production"]).
Point DATABASE_URL at staging or a disposable database.
Status
Alpha, but exercised end to end in CI against a rich multi-tenant schema and
run through pg_prove. The config shape may still shift before 1.0. Issues
and PRs welcome — see CONTRIBUTING.
Built by Matteo Perino while shipping GeoSuite, a multi-tenant Supabase app.
License
MIT — see LICENSE.
Related tools
Part of my open-source toolkit — github.com/matte97p:
- pentest-framework — low-noise pentest orchestration, normalized to one schema and rendered to a PDF
- demowright — record polished product-demo videos from a script that lives in your repo
- GeoSuite CLIs — zero-dep Generative Engine Optimization toolkit
⭐ If rlsgrid caught a leak (or proved you safe), give it a star — it helps other Supabase teams find it.
🌐 Built by Matteo Perino — matteoperino.dev
Release files for rlsgrid 0.5.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rlsgrid-0.5.7.tar.gz | 65.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rlsgrid-0.5.7-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 109.2 kB
Release files / rlsgrid-0.5.7.tar.gz
| Download URL | rlsgrid-0.5.7.tar.gz |
|---|---|
| Size | 65.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
36b5226639167d8ba3526c05bbea1660ce0fc80f6f01176b7c20bb5c8ecbb407
|
|
BLAKE2b-256 checksum How to use checksums |
46d56df1e561d8587b63975978271bf72fc03e65a3cfe3c3963dacdd756d5e48
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
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 Jul 11, 2026.
Transparency logRelease files / rlsgrid-0.5.7-py3-none-any.whl
| Download URL | rlsgrid-0.5.7-py3-none-any.whl |
|---|---|
| Size | 43.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ccf5992c34bbf65377fde09f74b2396234d9cd521d40f86307f739ae38f9083d
|
|
BLAKE2b-256 checksum How to use checksums |
95e5a8fcde842c61f01fb34a65eb1e1d5055e1e8227e185d858bcc567e8a7b0b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
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 Jul 11, 2026.
Transparency log