guptaaman678/supabase-grants-lint

Lint Supabase migrations for tables the Data API cannot reach once auto-grants end on 2026-10-30. Not affiliated with Supabase.

See the code

See what people are saying

README

supabase-grants-lint

Find the Supabase migrations that break on a fresh environment: tables the Data API cannot reach (PostgREST 42501 permission denied for table) once new tables stop getting automatic grants.

npm ci OpenSSF Scorecard mutation testing license: MIT

supabase-grants-lint check reports three missing grants, the printed fixes are appended, and a second check is clean

It replays your SQL migrations, works out who can reach every table, and prints the grant that fixes each finding. It reads files only: no database connection, no telemetry.

Quick start

In a project with a supabase/migrations folder (Node 22 or later):

npx supabase-grants-lint check
npx supabase-grants-lint doctor

Run it from your project root, the folder that contains supabase/, or pass --dir.

  • check lints the migrations and exits 1 on an error finding.
  • doctor is a readiness report for 2026-10-30: whether the project is opted in, whether replaying the history turns automatic grants back on, and which existing tables a fresh database would not expose.

To run it on every pull request:

npx supabase-grants-lint init --since next

This writes grants-lint.config.json and .github/workflows/grants-lint.yml. --since next enforces every migration you add from now on and leaves the existing ones alone. Commit both files and open a pull request: the check runs on it.

What breaks on 2026-10-30

Supabase has announced that from 2026-10-30 new tables, views and sequences in public stop getting automatic grants for anon, authenticated and service_role on every existing project. Preview branches already work this way, and so can new projects and local stacks with auto_expose_new_tables = false. A migration that creates a table and forgets to grant applies cleanly, row level security looks right, and the first request fails. Five traps are easy to miss:

  • Replaying history turns the grants back on. A baseline made with supabase db pull can contain alter default privileges ... grant all, so local resets and preview branches give new tables grants production no longer has (GL007).
  • The revoke is narrow. Per the SQL Supabase published, it removes select, insert, update and delete (and sequence usage, select), and leaves truncate, references and trigger (plus maintain on Postgres 17+) on every new table (GL008).
  • service_role bypasses row level security, not grants. Edge functions and admin tools using the service role key get 42501 too (GL001).
  • Policies without grants are dead. create policy ... to authenticated on a table authenticated holds no grant on never applies (GL002, GL003).
  • serial columns need their sequence. A client that inserts into a table with a serial column needs usage on its sequence; identity columns and uuid keys do not (GL004).

GitHub Action

name: Grants lint

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read
  security-events: write

jobs:
  grants-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: guptaaman678/supabase-grants-lint@v0

Findings show up as annotations on the pull request and in the Security tab (SARIF). Inputs, outputs and private-repository notes: docs/github-action.md.

Rules

RuleNameDefaultCatches
GL000no-enforcement-baselinewarnNo opt-in migration and no since, so the new-relation rules do not run
GL001missing-service-role-granterrorA new relation service_role cannot read or write
GL002unreachable-new-relationerrorA new relation with policies for a client role that holds no grant on it
GL003dead-policyerrorA policy whose roles lack the privilege its command needs
GL004serial-sequence-usageerrorA client-insertable serial column whose sequence the client cannot use
GL005blanket-grantwarngrant ... on all tables in schema re-granting every relation
GL006default-privileges-regranterroralter default privileges ... grant turning automatic grants back on
GL007replay-reenables-defaultswarnA replay of the history ends with automatic grants production does not have
GL008leftover-privilegeswarntruncate, references, trigger left to anon or authenticated
PARSE001unparseable-statementinfoA statement the Postgres parser rejects (skipped)
PARSE002dynamic-sql-skippedinfoA DO block or execute that changes grants or tables (not modelled)

Each page shows a failing example, the real output, the fix and when it is safe to disable the rule.

Configuration

Everything is optional. Put settings in grants-lint.config.json (or package.json#grantsLint); the most useful ones:

{
  "$schema": "https://raw.githubusercontent.com/guptaaman678/supabase-grants-lint/main/schema/config.schema.json",
  "since": "20261001000000",
  "schemas": ["public"],
  "serviceOnly": ["public.audit_log"],
  "rules": { "GL005": "error" }
}
  • since: enforce only migrations after this version. "auto" (default) finds your opt-in migration; set a version if the project was opted in from the dashboard or on 2026-10-30.
  • schemas: every schema you expose through the Data API.
  • serviceOnly: tables only server code uses, exempt from the client-role rules.
  • rules, ignore and inline -- grants-lint-disable-next-line GL002: <reason> comments turn findings off, always with a reason.

All keys and flags: docs/configuration.md. Other commands: supabase-grants-lint explain public.todos prints the grant timeline of one table, and --format json|sarif|github changes the output.

FAQ

"Does it touch my database?", "Why does it flag my old migrations?", "How is it different from splinter and squawk?": see the FAQ.

How it works

The migrations are parsed with the real Postgres parser and replayed, in order, into a model of which role holds which privilege on which table and sequence, including default privileges and row level security policies. Each migration is checked against that model at its end, so a grant later in the same file counts and a grant in a later file does not. Details and limitations (dynamic SQL, objects created outside migrations): docs/how-it-works.md.

Not affiliated with or endorsed by Supabase.

License

MIT

cli
github-action
grants
linter
migrations
postgres
postgresql
rls
security
supabase

guptaaman678/supabase-grants-lint

Lint Supabase migrations for tables the Data API cannot reach once auto-grants end on 2026-10-30. Not affiliated with Supabase.

See the code

See what people are saying

README

supabase-grants-lint

Find the Supabase migrations that break on a fresh environment: tables the Data API cannot reach (PostgREST 42501 permission denied for table) once new tables stop getting automatic grants.

npm ci OpenSSF Scorecard mutation testing license: MIT

supabase-grants-lint check reports three missing grants, the printed fixes are appended, and a second check is clean

It replays your SQL migrations, works out who can reach every table, and prints the grant that fixes each finding. It reads files only: no database connection, no telemetry.

Quick start

In a project with a supabase/migrations folder (Node 22 or later):

npx supabase-grants-lint check
npx supabase-grants-lint doctor

Run it from your project root, the folder that contains supabase/, or pass --dir.

  • check lints the migrations and exits 1 on an error finding.
  • doctor is a readiness report for 2026-10-30: whether the project is opted in, whether replaying the history turns automatic grants back on, and which existing tables a fresh database would not expose.

To run it on every pull request:

npx supabase-grants-lint init --since next

This writes grants-lint.config.json and .github/workflows/grants-lint.yml. --since next enforces every migration you add from now on and leaves the existing ones alone. Commit both files and open a pull request: the check runs on it.

What breaks on 2026-10-30

Supabase has announced that from 2026-10-30 new tables, views and sequences in public stop getting automatic grants for anon, authenticated and service_role on every existing project. Preview branches already work this way, and so can new projects and local stacks with auto_expose_new_tables = false. A migration that creates a table and forgets to grant applies cleanly, row level security looks right, and the first request fails. Five traps are easy to miss:

  • Replaying history turns the grants back on. A baseline made with supabase db pull can contain alter default privileges ... grant all, so local resets and preview branches give new tables grants production no longer has (GL007).
  • The revoke is narrow. Per the SQL Supabase published, it removes select, insert, update and delete (and sequence usage, select), and leaves truncate, references and trigger (plus maintain on Postgres 17+) on every new table (GL008).
  • service_role bypasses row level security, not grants. Edge functions and admin tools using the service role key get 42501 too (GL001).
  • Policies without grants are dead. create policy ... to authenticated on a table authenticated holds no grant on never applies (GL002, GL003).
  • serial columns need their sequence. A client that inserts into a table with a serial column needs usage on its sequence; identity columns and uuid keys do not (GL004).

GitHub Action

name: Grants lint

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read
  security-events: write

jobs:
  grants-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: guptaaman678/supabase-grants-lint@v0

Findings show up as annotations on the pull request and in the Security tab (SARIF). Inputs, outputs and private-repository notes: docs/github-action.md.

Rules

RuleNameDefaultCatches
GL000no-enforcement-baselinewarnNo opt-in migration and no since, so the new-relation rules do not run
GL001missing-service-role-granterrorA new relation service_role cannot read or write
GL002unreachable-new-relationerrorA new relation with policies for a client role that holds no grant on it
GL003dead-policyerrorA policy whose roles lack the privilege its command needs
GL004serial-sequence-usageerrorA client-insertable serial column whose sequence the client cannot use
GL005blanket-grantwarngrant ... on all tables in schema re-granting every relation
GL006default-privileges-regranterroralter default privileges ... grant turning automatic grants back on
GL007replay-reenables-defaultswarnA replay of the history ends with automatic grants production does not have
GL008leftover-privilegeswarntruncate, references, trigger left to anon or authenticated
PARSE001unparseable-statementinfoA statement the Postgres parser rejects (skipped)
PARSE002dynamic-sql-skippedinfoA DO block or execute that changes grants or tables (not modelled)

Each page shows a failing example, the real output, the fix and when it is safe to disable the rule.

Configuration

Everything is optional. Put settings in grants-lint.config.json (or package.json#grantsLint); the most useful ones:

{
  "$schema": "https://raw.githubusercontent.com/guptaaman678/supabase-grants-lint/main/schema/config.schema.json",
  "since": "20261001000000",
  "schemas": ["public"],
  "serviceOnly": ["public.audit_log"],
  "rules": { "GL005": "error" }
}
  • since: enforce only migrations after this version. "auto" (default) finds your opt-in migration; set a version if the project was opted in from the dashboard or on 2026-10-30.
  • schemas: every schema you expose through the Data API.
  • serviceOnly: tables only server code uses, exempt from the client-role rules.
  • rules, ignore and inline -- grants-lint-disable-next-line GL002: <reason> comments turn findings off, always with a reason.

All keys and flags: docs/configuration.md. Other commands: supabase-grants-lint explain public.todos prints the grant timeline of one table, and --format json|sarif|github changes the output.

FAQ

"Does it touch my database?", "Why does it flag my old migrations?", "How is it different from splinter and squawk?": see the FAQ.

How it works

The migrations are parsed with the real Postgres parser and replayed, in order, into a model of which role holds which privilege on which table and sequence, including default privileges and row level security policies. Each migration is checked against that model at its end, so a grant later in the same file counts and a grant in a later file does not. Details and limitations (dynamic SQL, objects created outside migrations): docs/how-it-works.md.

Not affiliated with or endorsed by Supabase.

License

MIT

cli
github-action
grants
linter
migrations
postgres
postgresql
rls
security
supabase

Languages

TypeScript

96.9%

JavaScript

2.9%