permission denied for table: the Supabase change, and the Postgres underneath
Short version: if a table you created after October 30, 2026 returns permission denied for table with code 42501, Supabase did what its changelog said it would do. New tables in public are no longer exposed to the Data API until you grant them. Existing tables keep the grants they already have. An app that connects as the table owner is unaffected; restricted database roles still need the appropriate grants. Fix the grant if your app uses supabase-js or the REST API. If it does not, you have been paying for a platform feature you never call, and a flat-rate Postgres is the smaller product.
I run Layerbase. We host Postgres, and this post is a competitor's reading of Supabase's own changelog. The facts below link to that changelog so you can check them without trusting me.
Contents
- What actually changed
- If your app uses the Data API
- If your app uses a connection string
- What else shipped the same week
- FAQ
What actually changed
Supabase published the plan on April 28, 2026, in Breaking Change: Tables not exposed to Data and GraphQL API automatically. Three dates matter:
- April 28: you could opt a new project out of automatic grants.
- May 30: that opt-out started becoming the default for new projects.
- October 30: the same default is applied to existing projects.
Once it reaches your project, a new table in public exists in Postgres and is invisible to the Data API until you GRANT it. Supabase is explicit that existing tables are not affected. They keep their current grants and stay reachable. The change also does not touch storage, auth, realtime, or a custom schema.
The Data API is the auto-generated REST and GraphQL layer. That is what supabase-js, the other client libraries, /rest/v1/, and /graphql/v1/ call. PostgREST answers a missing grant with a precise error rather than an empty result:
{
"code": "42501",
"message": "permission denied for table your_table",
"hint": "Grant the required privileges to the current role with: GRANT SELECT ON public.your_table TO anon;"
}That JSON is from the changelog. The hint names the role and the privilege. A grant and row-level security are different layers. The grant decides whether the role can touch the table at all. RLS decides which rows. Postgres rejects the query before RLS runs when the grant is missing, so a policy that looks correct still does nothing.
How to tell which mode a project is in: open Data API settings and look at Automatically expose new tables. If that is off, the next table you create needs an explicit grant. The changelog's rollout table is easy to misread. Its October 30 "user action" cell says tables without explicit grants stop being reachable. The body of the same post says existing tables keep working. Believe the body. Only new tables change.
Supabase's stated reason is that people are no longer the only ones creating tables. Agents, CLI scripts, and AI platforms create them too, often with nobody reviewing the diff. Automatic grants made a new table reachable from the anon key in a frontend before anyone had written a policy. Explicit grants make that a line in a migration.
If your app uses the Data API
Stay, and put the grant in the same migration as the table. Supabase's changelog treats three steps as one unit:
grant select on public.your_table to anon;
grant select, insert, update, delete on public.your_table to authenticated;
grant select, insert, update, delete on public.your_table to service_role;
alter table public.your_table enable row level security;
create policy "users can read their own rows"
on public.your_table
for select
to authenticated
using (auth.uid() = user_id);Grant only what each role needs. anon is the role behind your public key. Giving it insert, update, and delete is a product decision, not a default you copy from a snippet. The policy above is an example shape, not a policy for your schema. Write the one that matches the rows your app is allowed to see.
Do not "fix" a 42501 with grant ... on all tables in schema public. Supabase documents that statement as a rollback if you opted in early and want the old behavior back. It also grants every table in public, including ones you meant to keep private. Any of those with RLS off becomes reachable by anyone holding the anon key. Grant the new table. Revoke a private one by name.
This is the path for supabase-js, PostgREST, GraphQL, Lovable, and any other builder that talks to Supabase through the client libraries. The table is fine. The API cannot see it yet. Update the prompt or skill your agent uses so the next migration includes the grant. Supabase's changelog points at their agent skill for that.
If that is your app, you do not need a new database host. You need one GRANT and a policy. The rest of this post is for the other app.
If your app uses a connection string
The changelog tells you that you can stop reading. A direct connection as the table owner (through psql, Prisma, Drizzle, pg, or an app server with DATABASE_URL) is unaffected. Permissions depend on the database role, not the connection method: a restricted role still needs grants on new tables. If your production errors are 42501 from PostgREST, you are on the Data API whether or not you meant to be. A direct connection using a restricted role can also return 42501; check that role's grants before ruling out a permissions issue.
That is worth sitting with, because a Supabase project is two products sold as one.
One product is the platform: Auth, Storage, Realtime, Edge Functions, and the Data API, with Postgres underneath. If your UI calls supabase.from('invoices'), you bought that product, and the grant is part of using it. Leaving would mean rebuilding login, file storage, and subscriptions. Supabase alternatives is the longer version of when that rebuild is worth it, and the honest answer is often to stay.
The other product is Postgres. Your app already has a connection string. If your app connects as the table owner, the October 30 change leaves that access intact. You are paying for an API, an auth service, and a storage service your process does not call. The bill and the dashboard are shaped around the platform. The thing your app needs is a host, a port, and a database name.
That is the case Layerbase is for. It is managed Postgres with a normal connection string, at a flat price: Solo is $5/month and Pro is $15/month. Solo is the plan that keeps one small primary awake. A second database on the same plan can sleep. The always-on comparison has the limits, the shared region (US East), and the cases where a different host is the right call, including when you need high availability or a region outside the US.
What the move is, in practice:
- Point
DATABASE_URLat the new database. Prisma, Drizzle, andpgdo not change. - The Supabase migration copies your
publicschema, your rows, and yourauth.userspassword hashes in one read-only pass. User UUIDs stay the same, souser_idforeign keys still line up. The migration writeup is the step-by-step version. - Storage files, edge functions, realtime subscriptions, and active sessions do not come with the database. Plan those separately, or leave them where they are.
- Login does not come with it either. The bcrypt hashes copy byte for byte, and the Auth console can list and manage those users. Layerbase does not run GoTrue's hosted login endpoints. You wire a credential check against
auth.users, or you keep Supabase Auth and move only the database. Both are legitimate. Pretending the second is free is how migrations go wrong.
What I will not claim, because the same week made these words slippery:
- Layerbase is not a Data API. There is no anon key, no auto-generated REST table, and no RLS-as-your-backend. If you need those, stay on Supabase and add the grant.
- Layerbase is not high availability. A single primary failing over across zones is a different product, and the next section is about the one Supabase just opened.
- Point-in-time restore on Layerbase is for always-on Postgres on Pro and up. It restores to a new database. It is not a standby, and it is not on the Free plan. The PITR post is the mechanism and the drill.
If the app is a connection string and the region is acceptable, move the Postgres or create one and point the app at it. If you are on Lovable and the generated client is supabase-js, the grant is the fix. Connect a Postgres database to a Lovable app is the other shape, where the app already expects DATABASE_URL.
What else shipped the same week
October 28 through 30 was a loud week for Supabase, and none of those launches change the 42501 decision. They are for a different reader. Skim them so a benchmark headline does not talk you into a storage engine you cannot turn on later.
Multigres went to private alpha on October 29. It runs Postgres on three nodes across availability zones, promotes a replica on its own, and acknowledges a write only after synchronous replication can survive a failure. Paid orgs can request access. During the alpha it is free for up to two projects, it is not on the Free plan, and Supabase says pricing may change when it leaves alpha. The limits are the part to read before you treat it as a production plan: no uptime SLA, no point-in-time recovery (daily backups only), no Realtime, one region (East US, North Virginia), chosen only when you create the project, no managed path back to ordinary Postgres, and compute size fixed after creation. It also cannot be combined with OrioleDB.
That is the right product if losing the primary is unacceptable and you can live inside those limits while it is an alpha. It is not a reason for a small app to rebuild its host. Most applications on a single Postgres primary are fine until the day the machine or the zone is not, and that day is a real day. It is just not the day a new table started returning 42501.
OrioleDB is a public beta storage engine, on every plan, billed like ordinary Postgres. What is OrioleDB? is the longer explanation. You pick it when you create the project. You cannot add it to an existing project or remove it later. The October 28 post says the beta is not for production and has no SLA. Supabase's October 1 changelog says those projects do not get point-in-time recovery. The engine replaces heap storage with an undo log, which is aimed at update-heavy tables and at the vacuum work heap tables accumulate. Their DBArena numbers are theirs. Read them as a benchmark of a beta storage engine on large compute, then run your own workload if you are actually choosing it. A side project whose tables fit in memory is not the case those numbers describe.
DBArena, announced October 30, publishes hosted-Postgres benchmarks. Supabase sponsors it. The first round compares Supabase Postgres, Supabase with OrioleDB, Amazon RDS, and Google Cloud SQL, from a Small size (2 vCPU, 2 GiB) up to 8XLarge (32 vCPU, 128 GiB), on Postgres 17 with each provider's defaults, measured between September 25 and October 1. Throughput per dollar on a 32 vCPU box answers a capacity question. It does not answer what a quiet primary costs at 2am, which is the question a $5 or $15 plan is for. Layerbase is not in that table. I am not going to invent a ranking for a test we did not run.
FAQ
Does October 30 break tables I already have?
No. Supabase's changelog says existing tables keep their grants and stay reachable through the Data API. The change is the default for tables you create after the setting reaches your project.
My ORM can still see the new table, and the API cannot. Is the table missing?
The table is there. The role the Data API uses has no privilege on it yet. A direct Postgres connection may use a different role, such as the table owner; it still obeys that role's permissions. Grant the Data API role, or accept that this app does not use the Data API.
I use Lovable. Which fix is mine?
If the generated client is supabase-js, add the grant and RLS in the same migration. If the app connects with a Postgres URL as the table owner, this grant change does not affect it, and moving the database can preserve those calls. Restricted roles still need appropriate grants on the destination.
Will Layerbase replace Supabase Auth, Storage, and Realtime?
No. The migration can copy auth.users and the bcrypt hashes. It does not run GoTrue's login endpoints, and it does not move storage files, realtime, or edge functions. Keep those on Supabase, or replace them yourself. Migrating from Supabase to Layerbase lists what moves and what does not.
Do I get high availability or OrioleDB?
No. Multigres and OrioleDB are Supabase products, with the alpha and beta limits above. Layerbase is a single-primary Postgres host. If you need automatic failover across zones, evaluate Multigres or another HA service, and read the SLA separately from the topology. High availability does not undo a bad DELETE that replicated.
If you want the Postgres and not the API, start the Supabase migration. If you still need the Data API, add the grant and ignore the rest of this page.
Keep reading
- What is OrioleDB?OrioleDB is an open-source storage engine for Postgres. It replaces heap tables with an undo log so update-heavy tables stop leaving dead rows behind. It is a Supabase public beta, not a new database, and you cannot turn it on later.
- Cloudflare Workers with Postgres: setup, costs, and migrationRun a Cloudflare Worker against Postgres with Hyperdrive. A practical setup, the separate hosting and database costs, and what changes when you leave Supabase.
- Supabase is acquiring Turso. Read this if you build on your own.Supabase announced it is buying Turso, the company behind libSQL, alongside another $150M. Nothing breaks today. The part worth your attention is how few companies now own the default database for small projects, and what that does to pricing later.
- Supabase alternatives: what to use when the meter stops making senseSupabase is a good product with a metered bill and a Postgres-only ceiling. Here is the honest shortlist of alternatives, what each one is actually for, and when you should just stay.