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 (5)

Collapse
 
mattbusel profile image
Matt Busel •

Good call on the hint being the trap. An agent that self-corrects from that error will likely copy the GRANT line as written, and if it tested while logged out, that line says anon.

One check worth running before and after the 30th, since it catches both the old bulk grant and a hint copied too fast:

select c.relname
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r'
  and not c.relrowsecurity
  and has_table_privilege('anon', c.oid, 'select');
Enter fullscreen mode Exit fullscreen mode

Any row it returns is a table logged-out visitors can read in full through the Data API. Swap 'select' for 'update' or 'delete' to see the worse cases.

Do you know if the security advisor flags a table that has RLS on but a policy with using (true) for anon? That one passes the RLS-enabled check and still returns every row.

Collapse
 
rawilli profile image
Roy Williams •

Thanks Matt, that's actually a great point about the hint, bro. If an agent tests while logged out, anon is exactly what it'll copy.

To your question, I'd say yes. Security Advisor flags it with check 0024 ("permissive RLS policy"). It warns when a policy uses an always-true condition like using (true).

Hey Matt, I'm actually writing a "checks before you launch" post for later this year. Would you be interested if I included your query, with credit? Let me know.

Collapse
 
cekuu35 profile image
Cenk KURTOĞLU •

Solid write-up, and the grants-vs-RLS framing is exactly the distinction people get wrong when 42501 shows up and they "fix" it by reopening tables or relaxing policies.

One gotcha worth adding for the ALTER DEFAULT PRIVILEGES fix: default privileges attach to the role that runs the ALTER. If your migration runner executes as a different role than the one that later runs CREATE TABLE (a custom migration role vs postgres, for example), the defaults silently never fire for those tables. The SQL looks correct in review; the grants just aren't there.

A cheap verification loop:

  1. Run your defaults migration on a preview branch.
  2. Check what actually landed:
select grantee, table_name, privilege_type
from information_schema.role_table_grants
where table_schema = 'public'
  and grantee in ('anon', 'authenticated');
Enter fullscreen mode Exit fullscreen mode
  1. Create a throwaway table through your normal migration path, then hit it over REST with the anon key before promoting.

That catches the role-mismatch case that code review won't.

Collapse
 
shipmarkco profile image
Shipmark •

Worth repeating for anyone whose AI tool hits this error: the fix an AI reaches for first is the broad one. Name the role and the table, grant only what that role needs, and keep RLS on in the same step. A wide grant with no lock is just the open database with extra paperwork.

Collapse
 
shipmarkco profile image
Shipmark •

The agent rule at the end is the piece worth copying. An agent that fixes a 42501 with a broad grant has just undone the change on your behalf. Putting the per-table rule in the instructions file turns a one-time fix into a habit the tool keeps.