Your RLS policies look fine – yet the Supabase client throws:
permission denied for table projects
ERROR: 42501
Or PostgREST shows 42501 / “permission denied”. This is not the same as new row violates row-level security policy. Here is how to tell them apart and fix the grants.
The Problem: GRANT Runs Before RLS
Postgres checks privileges in layers. Table-level GRANTs for the role your request uses (anon or authenticated) are evaluated before Row Level Security. If the role has no privilege on the table, Postgres stops with 42501 – your policies never run.
Typical triggers:
- Table created via raw SQL / Prisma / Drizzle without grants to
authenticated/anon - Wrong key: calling with the anon key while only
authenticatedhas grants - Schema / table exposed in the API but privileges missing after a migration
If you instead see new row violates row-level security policy, you already passed grants – fix the policies (or use returning: 'minimal'). See the RLS insert error guide.
The Solution: Grant the API Roles Explicitly
Step 1: Confirm the Error Code
const { data, error } = await supabase.from('projects').select('*')
if (error) {
console.error(error.code, error.message)
// 42501 / "permission denied for table …" → grants
// "new row violates row-level security policy" → RLS policies
}
Step 2: Check Current Grants
SELECT grantee, privilege_type
FROM information_schema.role_table_grants
WHERE table_schema = 'public'
AND table_name = 'projects'
ORDER BY grantee, privilege_type;
You want the role that matches your client call (authenticated for logged-in users, anon only if anonymous access is intended).
Step 3: Grant Least Privilege
-- Logged-in users via the anon/publishable key + user JWT
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.projects TO authenticated;
-- Only if anonymous clients must read/write (rare for writes)
-- GRANT SELECT ON TABLE public.projects TO anon;
-- Sequences for serial / identity columns
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO authenticated;
Keep RLS enabled after granting. Grants open the door; policies decide which rows get through.
Step 4: Reload the API Schema (If Needed)
After creating tables or changing grants, PostgREST may need a schema reload (Dashboard → Settings → API → Reload, or wait for cache refresh). A missing grant can also look like “table not found in schema cache”.
Common Mistakes
- Writing more RLS policies for a 42501: policies never run until grants exist.
- Granting only to yourself / postgres: the SQL editor works; the JS client still fails.
- Broad grants to anon without RLS: security hole – prefer
authenticated+ tight policies. - Confusing with Storage 403: Storage uses
storage.objectspolicies – see Storage 403 RLS on upload.
Troubleshooting Checklist
- ✅ Error is
42501/permission denied for table(not RLS violate)? - ✅ Client uses the intended key + session?
- ✅
role_table_grantsshows privileges forauthenticated(oranon)? - ✅ RLS still enabled with real policies after the grant?
- ✅ API schema reloaded if the table is brand new?
Conclusion
permission denied for table is a missing GRANT, not a broken RLS policy. Grant the API role, keep RLS on, then tune policies. For the other classic message – insert blocked by RLS – use the dedicated row-level security policy fix.
Related articles
- Fix RLS: new row violates row-level security policy
- Fix infinite recursion detected in policy
- Supabase Storage 403 RLS on upload
- Supabase Auth: Invalid login credentials
- Supabase Edge Functions: Invalid JWT / 401
- Supabase RLS documentation
Still stuck on 42501? Drop the table name and whether you call as anon or authenticated in a comment – no secrets.
Comments