Skip to content

Build1 publisher3 min readPublished

Supabase's Oct 30 grant change leaves tables unreachable in 98 of 100 replayed migration histories

Ninety-eight of 100 public Supabase migration histories leave a table the Data API cannot reach once automatic grants end on Oct 30, a dev.to replay found. Existing tables keep their grants, so the failures land on new tables and on environments rebuilt from those files.

The Engineer · Build desk

Illustration accompanying Supabase's Oct 30 grant change leaves tables unreachable in 98 of 100 replayed migration histories

What happened

  • In 92 of the replayed histories, at least one row level security policy names a role that lacks the privilege its command needs, so the policy can never apply.
  • The change covers new tables, views and sequences in the public schema, for the anon, authenticated and service_role roles, on every existing Supabase project.
  • None of the 100 histories contains the opt-in migration Supabase published for the new grant behaviour.
  • A migration that omits the grant applies cleanly and its policy looks right in the dashboard, but the first request fails with Postgres error 42501, permission denied.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision Each new create-table migration now has to carry explicit grant statements per role, service_role included, next to the policies it already writes.
  • cost Edge functions, cron jobs and admin tools holding the service role key break on any new table nobody granted to service_role, and the break surfaces only when they run.
  • exposure Teams that want clients limited to what they granted have to write their own revokes, because Supabase's opt-in leaves privileges that row level security cannot restrict.

Postgres checks a role's table privilege before it evaluates any row level security policy [15]. Until now Supabase handled the privilege check with a default. When postgres created a table in public, anon, authenticated and service_role had access immediately [6]. The post's author wrote: "You wrote your row level security policies and forgot that grants existed at all." [17]

The two replay figures measure different things. The 98 counts a project if any one table is unreachable through the Data API for any one role [2]. A table kept away from anon on purpose would land in that count next to a genuine mistake. The 92 is harder to explain away. A policy that names a role is written intent, and in those histories the privilege its command needs is missing [3]. The sample is 100 public projects [2]. A private codebase gets the same result only if its migrations share the habit of writing policies without grants.

A common assumption, according to the post, is that the service role key can do anything [15]. It bypasses policies. But service_role is not a superuser, and Postgres checks the privilege first [15]. The author calls this the most common finding in the corpus, at 97 of the 100 histories [16].

Preview environments hide the failure. A first migration produced by `supabase db pull` probably contains `alter default privileges for role "postgres" in schema "public" grant all on tables` for each API role, because pg_dump copies default privileges as they stood at pull time [8]. Replaying that file on a preview branch or a local `supabase db reset` gives every later table full access again [9]. Supabase's branching docs say so, and production no longer has those defaults [9]. The local CLI also exposes new tables automatically unless `auto_expose_new_tables = false` is set under `[api]` in `supabase/config.toml` [10]. "A local test run tells you nothing about grants," the author wrote [18].

Supabase's own opt-in SQL revokes select, insert, update and delete on tables, and usage and select on sequences [12]. The old default was `grant all`. So truncate, references and trigger still go to anon and authenticated on every new table, with maintain added on Postgres 17 and later [13]. Before Postgres 17, that leaves three of the seven table privileges in place [1]. Row level security does not apply to truncate or references [13]. The author's fix is a per-table revoke of truncate, references and trigger from anon and authenticated [14].

I think Supabase has the default right. Grants and policies are separate checks in Postgres. The old default privilege passed the first check for every new table, so teams could write policies against access they never declared. The cost of the change falls on migration histories and the environments built from them [5]. For projects with a db pull baseline, the post recommends a later migration that switches the default grants off again [11]. Two of the 100 histories still end with default grants switched on by their own statements [11].

What to watch

  • Whether Supabase changes the CLI's auto_expose_new_tables default so local stacks match production grants after Oct 30.
  • Whether Supabase revises its published opt-in SQL to also revoke truncate, references and trigger from anon and authenticated.
  • Whether Supabase's branching tooling starts stripping the default-privilege lines that db pull baselines carry into preview branches.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories