DEV Community

Roy Williams
Roy Williams

Posted on AI-assisted

Supabase's October 30 change: don't fix "permission denied" by reopening your tables

On October 30, 2026, Supabase applies a new default to every existing project: new tables you create in the public schema are no longer reachable through the Data API until you explicitly grant access. New projects have worked this way since May 30.

This is a good change. It also creates a moment where a rushed fix can leave you worse off than before. Here is what changes, what you will see, and how to fix it without reopening your data.

What actually changes

Until now, every new table in public automatically received select, insert, update and delete privileges for the anon, authenticated and service_role roles. That made every new table reachable through supabase-js, REST and GraphQL the moment it existed, including tables nobody had protected yet.

After the change reaches your project:

  • Existing tables are not touched. They keep their current grants and keep working.
  • New tables in public need an explicit grant before the Data API can see them.
  • RLS is unchanged. Grants decide whether a role can touch a table at all. RLS decides which rows that role can see. You need both.
  • Direct Postgres connections are unaffected. If your app only uses psql, an ORM or a server with a connection string, this does not apply to you.
  • Other schemas are unaffected. storage, auth, realtime and custom schemas keep their current defaults.

What you will see

The first time your app (or your AI coding agent) creates a table and queries it, you get:

{
  "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;"
}
Enter fullscreen mode Exit fullscreen mode

The hint is helpful. It names the role that asked and the grant that would let it in. But read it carefully, because that is where the risk is.

The trap: three quick fixes that reopen your data

1. The bulk grant. Supabase's own rollback FAQ includes this, for people who opted in early and want the old behavior back:

grant select, insert, update, delete on all tables in schema public to anon, authenticated, service_role;
Enter fullscreen mode Exit fullscreen mode

That is a legitimate rollback, but as a fix for one broken table it reopens every table in public to every API role. If any of those tables lacks RLS or has a loose policy, it is exposed again.

2. Following the hint word for word. If the failing request was made by a logged-out visitor, the hint suggests granting to anon. Most app tables should never be readable by logged-out visitors. Ask "who should be able to reach this table?" before you copy the line.

3. Granting on a table that has no RLS. Tables created in the Supabase dashboard get RLS enabled by default. Tables created in the SQL editor or by other tools, including migrations and AI agents, need RLS enabled explicitly (Supabase docs). A grant on a table without RLS means any role with that grant can read every row.

This matters more now than it would have a year ago. Supabase's changelog says the hint exists partly so that "an agent can self-correct when a grant is missing." That is useful, but an agent that self-corrects by granting anon on an unprotected table has just undone the protection this change was designed to add.

The safe pattern: grant, RLS and policy as one unit

Supabase recommends treating these three steps as a unit, in the same migration:

-- 1. Grant only what each role needs
grant select, insert, update, delete on public.your_table to authenticated;
grant select, insert, update, delete on public.your_table to service_role;
-- grant select on public.your_table to anon;  -- only if logged-out visitors truly need to read it

-- 2. Enable RLS
alter table public.your_table enable row level security;

-- 3. Add the policies you need
create policy "users can read their own rows"
  on public.your_table
  for select
  to authenticated
  using (auth.uid() = user_id);
Enter fullscreen mode Exit fullscreen mode

If your app has organizations or teams, the policy should check membership in the row's organization, not only auth.uid(). Remember that insert and update policies also need a with check clause, or a user can write a row into someone else's organization.

If you build with an AI coding agent

Add a rule to your agent's instructions file (for example CLAUDE.md, a Cursor rule, or your system prompt):

When you create a table in the public schema, in the same migration: grant only the privileges each role needs (authenticated for signed-in users; anon only if I confirm logged-out visitors must read it, and then select only), enable row level security, and add policies that limit rows to their owner or their organization. Never use "grant ... on all tables in schema public" to fix a permission error. If you hit a 42501 error, tell me which role and table, and propose a per-table grant instead of applying it.

Supabase also maintains an agent skill that includes the grants step; the changelog recommends adopting it.

A 10-minute checklist before October 30

  1. Open Security Advisor and the Data API exposure badge in the Table Editor. They list tables worth reviewing.
  2. Try the new behavior early on a staging project. Supabase documents the opt-in SQL in its changelog.
  3. Update your migration templates so every new table ships with grants, RLS and policies together.
  4. Update your AI agent's instructions with the rule above.
  5. Revoke grants on any existing table that should not be reachable at all: revoke all on table public.your_table from anon, authenticated, service_role;
  6. After your next migration, check the outcome, not just the SQL. Sign in as a user from a different account or organization and confirm you cannot read or change the first user's rows.

The short version

The change is good for everyone. The risk is the fix. Grant per table, per role, with RLS and policies in the same migration, and then test that one signed-in user really cannot reach another's data.


I build RowAttest, which runs that last check on a staging Supabase project: it signs in as test users from different organizations and tries to read and write across them. The steps above work without it.

Top comments (0)