Fix: permission denied for table profiles in Supabase

Updated Oct 2026 · 6 min read

You created the profiles table, wrote RLS policies that look correct, and the client still returns:

{
  "code": "42501",
  "message": "permission denied for table profiles"
}

This is not an RLS problem. Postgres checks table privileges before it ever evaluates RLS policies. On newer Supabase projects, the anon and authenticated roles do not automatically receive privileges on tables you create in the public schema, so the request dies before your policy runs.

The two layers people confuse

GRANT answers "may this role touch this table at all?" RLS answers "which rows inside that table?" You need both. Most RLS tutorials only show layer two.

1. Grant privileges on the schema and the table

-- schema usage first: without it, table grants are unreachable
grant usage on schema public to anon, authenticated, service_role;

-- table privileges
grant select, insert, update, delete on public.profiles to authenticated;
grant select on public.profiles to anon;          -- only if anonymous reads are intended
grant all on public.profiles to service_role;     -- bypasses RLS, still needs grants

-- optional: apply to tables you add later
alter default privileges in schema public
  grant all on tables to service_role;
Why service_role still needs a grant: service_role bypasses row-level security, but it is still a Postgres role. Without table privileges it gets the same 42501 from server-side code — this is the failure that breaks webhooks after everything "worked" in the browser.

2. Keep the RLS policies

alter table public.profiles enable row level security;

create policy "read own profile"
  on public.profiles for select
  using (auth.uid() = id);

create policy "update own profile"
  on public.profiles for update
  using (auth.uid() = id)
  with check (auth.uid() = id);

3. Make the signup trigger work

A trigger that inserts into profiles on signup runs with the permissions of the function owner, so it needs security definer:

create function public.handle_new_user()
returns trigger
language plpgsql
security definer
set search_path = public
as $$
begin
  insert into public.profiles (id, email)
  values (new.id, new.email);
  return new;
end;
$$;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute function public.handle_new_user();

4. Verify instead of guessing

Run this in the SQL editor to execute as authenticated rather than as the admin user:

begin;
set local role authenticated;
select count(*) from public.profiles;
rollback;

If that returns a number instead of 42501, the grants are correct and any remaining blank result is an RLS policy issue.

Free: the Supabase + Stripe launch checklist — all 23 checks in one page, no email required to read the guides.

Get the free checklist

This exact schema ships in the starter

Grants, RLS policies, the handle_new_user trigger and Stripe subscription sync — already wired and tested end to end.

Get the starter — $29 Try the live demo