DEV Community

FlareCanary
FlareCanary

Posted on

Supabase's May 30 Default Flip: Your Newly Created Tables Will Stop Reaching the Client With permission denied for table

Two Supabase changes in May change the rules for how new tables and the GraphQL endpoint reach your client. Both are documented in the Supabase changelog, both have explicit cutoff dates, and both will silently land in production for teams that aren't watching.

  • May 18, 2026pg_graphql is no longer enabled by default. New projects ship without the extension; existing projects with zero GraphQL traffic over the prior 30 days get it disabled. (Jan 26 changelog.)
  • May 30, 2026 — "Automatically expose new tables and functions" is OFF by default for all newly created projects. New tables in public are no longer auto-granted to the API roles anon, authenticated, service_role. (Apr 28 changelog.) The same rule reaches existing projects on October 30, 2026.

The interesting one is May 30. The pg_graphql change is loud — your /graphql/v1 endpoint returns extension-not-found and you fix it in five minutes. The exposure-default flip has a different shape, the one I keep writing this column about: the migration applies, the table exists, the dashboard shows it, and your client gets a 403 the first time it ships.

This is the thirteenth provider in the running silent-breakage tally, and the failure mode pairs with the Kubernetes 1.36 gitRepo article — every pre-deploy gate passes, the failure surface is post-apply.

What Actually Flips on May 30

The mechanism isn't a feature flag in your supabase/config.toml. It's PostgREST default privileges. The "expose new tables" toggle, when on, runs an equivalent of:

alter default privileges for role postgres in schema public
  grant select, insert, update, delete on tables to anon, authenticated, service_role;
Enter fullscreen mode Exit fullscreen mode

When off — the new default for any project created on or after May 30 — that grant doesn't fire. Your migration creates a table, the table is fully real in Postgres, but PostgREST's API roles can't read it.

-- supabase/migrations/20260601_add_invoices.sql, ships fine, applies fine:
create table invoices (
  id uuid primary key default gen_random_uuid(),
  customer_id uuid not null,
  amount_cents integer not null,
  created_at timestamptz default now()
);
Enter fullscreen mode Exit fullscreen mode

The first call from the SDK against the new project:

const { data, error } = await supabase.from('invoices').select();
// data: null
// error: {
//   code: '42501',
//   message: 'permission denied for table invoices',
//   hint: 'Grant the required privileges...',
//   details: null
// }
Enter fullscreen mode Exit fullscreen mode

Postgres error code 42501 is insufficient_privilege. That's the string developers Google. The error is loud — a 403 from /rest/v1/invoices — but it's loud in production after deploy, not in CI, not in npx supabase start.

Why Local and Tests Don't Catch It

The whole point of the local Supabase CLI stack is that it mirrors prod. It mostly does — but the new default flip is a project-creation-time setting, and the local stack inherits the old behavior. So:

  • npx supabase start runs PostgREST against a local Postgres seeded with the legacy default privileges. Migrations from your supabase/migrations/ directory get the auto-grant. Local dev queries succeed.
  • Your CI runs the same local stack. Tests pass.
  • Production projects created before May 30 also keep the old behavior — until October 30, when existing projects get migrated. So the same migration that worked on a March-created project will silently 403 on a June-created sibling project, even with identical code.
  • The dashboard shows the table in the Table Editor. Schema is correct. RLS policies you wrote apply correctly to the API roles — once the underlying grants exist. Without grants, the RLS check never happens; PostgREST short-circuits at the privilege layer.

The rough mental model: your test environment is a March 2026 project. Your new prod environment, the staging clone you spun up after May 30, the per-customer DB-per-tenant project you provision in your onboarding flow — those are June 2026 projects, and they need explicit grants.

The supabase-js Failure Shape

supabase-js surfaces this as a structured error, not an exception. Code that reads data without checking error quietly receives null:

// Looks reasonable, ships often:
const { data } = await supabase.from('invoices').select();
return data ?? [];   // returns [] forever in the new default
Enter fullscreen mode Exit fullscreen mode

That pattern is everywhere. If your render layer treats empty [] as "no data," the page loads, the empty-state UI shows, and nobody on the team realizes the API returned a 403 — they think the table is empty. The error is in the response, but unreached.

Same shape in supabase-py:

res = supabase.table("invoices").select("*").execute()
# res.data is None
# res.error is {'code': '42501', 'message': 'permission denied for table invoices', ...}
Enter fullscreen mode Exit fullscreen mode

Same in supabase-flutter, supabase-swift, and any direct fetch against /rest/v1/*. There's no SDK version that escapes it — the change is server-side, in PostgREST role grants.

What pg_graphql Disablement Looks Like (May 18)

Different shape, easier to fix. Code that hits /graphql/v1 or uses .schema('graphql_public').rpc('graphql', ...) gets either an extension-not-found error or a 404, depending on which endpoint your SDK chose. The fix:

create extension if not exists pg_graphql;
Enter fullscreen mode Exit fullscreen mode

Or in the dashboard: Database → Extensions → enable pg_graphql. Once enabled, the extension behaves identically to before. The only nuance is that existing projects with zero GraphQL traffic over the prior 30 days get it auto-disabled on May 18, so a project that was working could go dark if its GraphQL traffic was thin.

Pair this with the May 30 grant change and a brand-new project gets to do both: enable the extension manually and grant the API roles read access on every table you want exposed.

Why Schema Drift Tools and Schema Validators Don't Help

This is the awkward part for anyone who already has guardrails on their database changes:

  • Migration linters (sqlfluff, the Supabase advisor's lint pass) read the migration SQL and tell you the schema is sound. The schema is sound. The grants aren't the schema.
  • Type generators (supabase gen types typescript) introspect the schema, write Database['public']['Tables']['invoices'], and your TypeScript compiles. The generator reads structure, not privileges.
  • Zod / Pydantic / runtime validators that wrap the SDK response don't fire — data is null, error is populated, no schema gets validated.
  • Smoke tests against staging catch it only if staging is a post-May-30 project. If staging was created in March, it has the old default and won't reproduce the failure.

The Fix You Want in Every New Migration

The pragmatic shape for migrations going forward — write the grants explicitly, even on projects that still have the auto-grant default. Your migration becomes portable across project creation dates:

create table invoices (
  id uuid primary key default gen_random_uuid(),
  customer_id uuid not null,
  amount_cents integer not null,
  created_at timestamptz default now()
);

-- Make this migration work on projects created after May 30, 2026:
grant select on public.invoices to anon;
grant select, insert, update, delete on public.invoices to authenticated;
grant select, insert, update, delete on public.invoices to service_role;

-- Plus your RLS policies as before:
alter table invoices enable row level security;
create policy "users see own invoices" on invoices
  for select to authenticated using (customer_id = auth.uid());
Enter fullscreen mode Exit fullscreen mode

If you want the auto-grant default back on a specific project — Database Settings → "Automatically expose new tables and functions" — toggle on. That just re-installs the default privileges rule shown above; it does not retroactively grant existing tables.

Your Security Advisor in the dashboard will flag missing grants on tables you have RLS policies for. Read that panel. It is the only place the system tells you "this table has policies but isn't reachable."

How to Detect This Class of Change

The general defense, restated for the database flavor of silent breakage:

  1. Spin up a fresh Supabase project as part of your CI for migration tests. Not a local stack — an actual Supabase-hosted project, created today. The local stack inherits the old defaults and won't reproduce. A new project will. (For most teams this is a per-PR ephemeral project against a free-tier org.)
  2. Watch for 42501 errors at the edge. Add a request-level log on PostgREST 403s with body content. The shape is consistent enough to alert on.
  3. Check your tables list against your grants. A nightly query that diffs pg_class entries against information_schema.role_table_grants for the API roles will surface tables that exist but aren't reachable. Run it on every project, not just dev.
  4. Pin the project creation date in your runbook. "All our prod projects were created in 2025" matters now in a way it didn't last quarter. October 30, 2026 is the date that flips this for all existing projects too.

The Pattern, Now Thirteen Months In

Provider Surface What Goes Wrong
Stripe Basil Subscription.current_period_end Moved to items[]; old reads return undefined
GitHub pull_request.merge_commit_sha Returns null on closed PRs in 2026-03-10 ver
GitHub Org security fields PATCH returns 200, applies nothing
OpenAI Responses input_text Rejected with Invalid value error
HubSpot Contacts v1 endpoints Return 200 with list-memberships silently dropped
Auth0 TLS handshake Weak ciphers start returning handshake_failure Jun 10
Twilio api.de1.twilio.com Removed; regional domains never actually routed regionally
Shopify Checkout metafields Returns undefined after 2026-04; orders ship without app data
Kubernetes 1.36 gitRepo volumes Pass validation, fail at deploy with FailedMount
Anthropic claude-3-haiku-20240307 Returns model-retired error after Apr 20
OpenAI DALL·E 2/3 Retired May 12; per-image billing flips to per-token
Exa /research + crawl-date filters 404, parameters silently ignored, fields null
OpenAI Realtime Audio/text/transcript event names Renamed; old listeners silently never fire
Supabase PostgREST default privileges New tables 403 with 42501; CI doesn't repro

Fourteen providers. Same shared shape: the system answers, the SDK is fine, the thing your code expects to reach is gated by something your local environment doesn't model.

If you operate Supabase projects, the action items are short. New migrations: ship the grants explicitly. New projects after May 30: enable pg_graphql if you use it, and turn on auto-exposure if you'd rather not write grants per table. Every project: read the Security Advisor panel. October 30 is the date this hits projects you've already deployed against.

What I'm Building

I'm working on FlareCanary for exactly this class of post-deploy surprise. Point it at the API surfaces your application depends on — REST, GraphQL, and the response shape — and it polls them on a schedule, learns the response vocabulary, and alerts when a field disappears, a status code flips, or the privilege layer starts answering 403 where it used to answer rows. Free tier covers up to five endpoints, useful for keeping a watch on a Supabase project alongside the upstream APIs you call.

You don't need a tool for this. You do need a habit. The Supabase changelog covers both May cutoffs in plain language. Anyone reading it will catch them. The half-broken state still ships somewhere — to a team that provisions a per-tenant project from a script, to a team whose staging is a different vintage from prod, to a team that trusted local dev to model production privileges.

That's the gap. The schema being correct isn't enough. The grants matter.


If your Supabase project trips on the May 30 default flip — or if a 42501 permission denied for table error catches you off guard — I'd like to hear about it. The "code is right, schema is right, privileges aren't" failures are exactly the ones I'm tracking. Drop a comment or reach out.

Top comments (0)