Free tool · no account

Audit row-level security on any Postgres

Fourteen checks for the failures that make a Postgres database leak in practice — not the ones that are easy to check for. Works on Supabase, Neon, RDS, Cloud SQL, or a container on your laptop. One command, nothing to install, nothing to sign up for.

~npx @rebasepro/rls-check "postgresql://user:pass@host:5432/db"
Read-only
SELECTs against the system catalogues and nothing else. It never reads your data.
Nothing leaves the machine
No telemetry, no upload, no network beyond the connection string you pass it.
No Rebase required
It reads the database, not your codebase. Most people who run it do not use Rebase.

The fourteen checks

Each finding the tool prints carries the id below, the reason it matters, and the SQL that fixes it. Severity is the worst level a check can report — several grade themselves down depending on what they find.

Critical6 checks

Table exposed without row-level security

rls-disabled

A table with RLS disabled that also grants SELECT/INSERT/UPDATE/DELETE to a role an unauthenticated or untrusted caller can reach.

How to fix it →

Policy grants unconditional access

policy-always-true

A permissive policy whose USING or WITH CHECK expression is always true.

How to fix it →

Policy only checks that a caller id exists

policy-anonymous-tautology

A policy whose expression is `auth.uid() IS NOT NULL`-shaped: it separates signed-in from signed-out callers but scopes no rows.

How to fix it →

View reads past its base table's RLS

view-bypasses-rls

A view granted to an untrusted role that selects from an RLS-protected table and runs with its owner's privileges instead of the caller's.

How to fix it →

Materialized view exposes RLS-protected data

matview-bypasses-rls

A materialized view granted to an untrusted role whose defining query reads a table with row-level security enabled.

How to fix it →

Unauthenticated callers can write

anonymous-write-allowed

A permissive INSERT/UPDATE/DELETE policy reachable without authentication whose check expression accepts any row, backed by a matching grant.

How to fix it →
High2 checks

Unqualified column inside a policy subquery

unqualified-column-in-subquery

A bare column name in an EXISTS/IN subquery that exists on both the inner relation and the policy's own table, so Postgres binds it to the inner one.

How to fix it →

Many-to-many join table without RLS

junction-table-unprotected

A table that is essentially just two foreign keys, both pointing at RLS-protected tables, that has no row-level security of its own.

How to fix it →
Medium6 checks

RLS enabled but not forced for the table owner

rls-enabled-not-forced

A table with RLS enabled where the owning role is exempt from its own policies.

How to fix it →

RLS enabled with no policies (denies everything)

rls-enabled-no-policies

A table with row-level security enabled but not a single policy defined.

How to fix it →

Policies target roles nothing connects as

policy-role-unreachable

Every policy on a table names roles that do not exist, cannot log in, and that no login role inherits — so no policy ever applies and the table reads as empty.

How to fix it →

Table privileges granted to PUBLIC

grant-to-public

A SELECT/INSERT/UPDATE/DELETE privilege granted to PUBLIC on a table.

How to fix it →

SECURITY DEFINER routine with a mutable search_path

security-definer-mutable-search-path

A SECURITY DEFINER function or procedure that does not pin search_path, so the caller controls how its identifiers resolve.

How to fix it →

Policy calls current_setting() without missing_ok

current-setting-throws

A policy expression calling current_setting('x') with one argument, which raises rather than returning NULL when the setting is unset.

How to fix it →

What a run looks like

Every finding names the relation, says who can reach it and what they would get, and prints the SQL that closes it. Exits non-zero above a severity you choose, so it belongs in CI.

rls-check · production
$ npx @rebasepro/rls-check $DATABASE_URL

rls-check 0.1.2  ·  read-only Row-Level Security audit
────────────────────────────────────────────────────────────────

Database  db.acme.internal:5432/production
Server    PostgreSQL 16.4
Platform  Supabase
Scanned   3 schemas · 41 tables · 26 policies · 14 checks

CRITICAL
────────────────────────────────────────────────────────────────

  [critical] rls-disabled  public.invoices 
      public.invoices has row-level security disabled and is
      granted to anon and authenticated
      Impact  Any client holding the anon key can read — and
              write — every row in this table.
      Fix
          ALTER TABLE public.invoices ENABLE ROW LEVEL SECURITY;

  [critical] policy-always-true  public.documents, policy "documents_read" 
      USING (true) grants every row to every role the policy
      applies to.

HIGH
────────────────────────────────────────────────────────────────

  [high] view-bypasses-rls  public.billing_overview 
      The view runs as its owner, so it reads past the RLS on
      public.invoices.

────────────────────────────────────────────────────────────────
Summary

  2 critical · 1 high · 3 medium · 0 low
Questions

Questions about rls-check

Is it safe to run against a production database?

It is read-only, and narrowly so. It issues SELECTs against the system catalogues — pg_class, pg_policies, pg_proc, information_schema — and nothing else. It never reads your data, writes nothing, changes no setting, and makes no network request beyond the connection string you give it. There is no telemetry and no upload.

Do I need to be using Rebase?

No. It knows nothing about your framework and asks nothing of your codebase — it reads the database. It works against Supabase, Neon, RDS, Cloud SQL, a container on your laptop, or anything else that speaks the Postgres wire protocol. Most people who run it are not Rebase users.

What does it need to connect as?

A role that can read the catalogues — usually a superuser, the table owner, or something with BYPASSRLS. That is deliberate and the report says so at the top: the scan reads the true catalogue precisely because row-level security cannot constrain it, so the findings describe what *other* roles experience, not what this connection does.

How is this different from Supabase's own linter?

There is real overlap on the obvious cases, and both are worth running. The checks here that tend not to appear elsewhere are the indirect ones: a view or materialized view reading past its base table's RLS because it runs as its owner, a many-to-many join table left unprotected between two protected endpoints, and a bare column inside an EXISTS subquery that Postgres binds to the inner table — which turns a tenant filter into a tautology while looking completely correct.

Can I run it in CI?

Yes — that is the intended home for it. It exits non-zero when findings at or above a threshold you choose are present, emits JSON for machine consumption, and supports skipping individual checks by id so a deliberate exception does not fail every build. The ids are a stable public API for exactly this reason.

Why is a backend company giving away a security scanner?

Because the argument Rebase makes is that authorization belongs in the database rather than in the layer in front of it, and a tool that audits Postgres RLS is that argument in a form you can run against your own data instead of read. It is genuinely useful whether or not you ever look at the rest of this site — and if it finds nothing, that is a good afternoon and you owe us nothing.

Then fix it once, in code.

A scanner tells you where the policies are wrong. Rebase compiles them from your collection definitions, so the next one is reviewed in a pull request before it ever reaches a database.

~pnpm dlx @rebasepro/cli init