Lint Supabase migrations for tables the Data API cannot reach once auto-grants end on 2026-10-30. Not affiliated with Supabase.
TypeScript
0
65 commits
updated Sep 29, 2026
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.

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.
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.
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:
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).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).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).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.
| Rule | Name | Default | Catches |
|---|---|---|---|
| GL000 | no-enforcement-baseline | warn | No opt-in migration and no since, so the new-relation rules do not run |
| GL001 | missing-service-role-grant | error | A new relation service_role cannot read or write |
| GL002 | unreachable-new-relation | error | A new relation with policies for a client role that holds no grant on it |
| GL003 | dead-policy | error | A policy whose roles lack the privilege its command needs |
| GL004 | serial-sequence-usage | error | A client-insertable serial column whose sequence the client cannot use |
| GL005 | blanket-grant | warn | grant ... on all tables in schema re-granting every relation |
| GL006 | default-privileges-regrant | error | alter default privileges ... grant turning automatic grants back on |
| GL007 | replay-reenables-defaults | warn | A replay of the history ends with automatic grants production does not have |
| GL008 | leftover-privileges | warn | truncate, references, trigger left to anon or authenticated |
| PARSE001 | unparseable-statement | info | A statement the Postgres parser rejects (skipped) |
| PARSE002 | dynamic-sql-skipped | info | A 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.
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.
"Does it touch my database?", "Why does it flag my old migrations?", "How is it different from splinter and squawk?": see the FAQ.
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.
TypeScript
96.9%
JavaScript
2.9%
Lint Supabase migrations for tables the Data API cannot reach once auto-grants end on 2026-10-30. Not affiliated with Supabase.
TypeScript
0
65 commits
updated Sep 29, 2026
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.

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.
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.
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:
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).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).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).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.
| Rule | Name | Default | Catches |
|---|---|---|---|
| GL000 | no-enforcement-baseline | warn | No opt-in migration and no since, so the new-relation rules do not run |
| GL001 | missing-service-role-grant | error | A new relation service_role cannot read or write |
| GL002 | unreachable-new-relation | error | A new relation with policies for a client role that holds no grant on it |
| GL003 | dead-policy | error | A policy whose roles lack the privilege its command needs |
| GL004 | serial-sequence-usage | error | A client-insertable serial column whose sequence the client cannot use |
| GL005 | blanket-grant | warn | grant ... on all tables in schema re-granting every relation |
| GL006 | default-privileges-regrant | error | alter default privileges ... grant turning automatic grants back on |
| GL007 | replay-reenables-defaults | warn | A replay of the history ends with automatic grants production does not have |
| GL008 | leftover-privileges | warn | truncate, references, trigger left to anon or authenticated |
| PARSE001 | unparseable-statement | info | A statement the Postgres parser rejects (skipped) |
| PARSE002 | dynamic-sql-skipped | info | A 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.
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.
"Does it touch my database?", "Why does it flag my old migrations?", "How is it different from splinter and squawk?": see the FAQ.
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.
TypeScript
96.9%
JavaScript
2.9%