Supabase client permission denied for schema public – fix
PostgreSQL in Production

Supabase client permission denied for schema public – fix

Learn the exact steps to grant the right permissions in Supabase and stop the 'permission denied for schema public' error from breaking your app.

2026-06-02
7 min read
Supabase client permission denied for schema public – fix

TL;DR#

If you're seeing permission denied for schema public, the cause is usually missing USAGE/SELECT grants on the public schema for the role your client uses. Fix it by granting the proper privileges to the anon and/or authenticated roles.

If that doesn't work, scroll to verify the fix — there are two common variants this guide also covers.

What you'll see#

ERROR: permission denied for schema public
SQL state: 42501

It happens when a Supabase client (usually the JavaScript SDK) tries to select * from my_table right after a fresh deployment or when you add a new table without updating role permissions. The behavior is the same across local dev, Vercel preview, and production environments.

Root cause#

Supabase creates two built‑in Postgres roles: anon for unauthenticated requests and authenticated for logged‑in users. By default those roles have USAGE on the public schema, but if you have altered the default RLS policies, disabled the public schema, or manually revoked privileges, the roles lose the ability to see any object inside public. When the SDK issues a simple SELECT the database checks two things:

  1. Does the role have USAGE on the schema? Without it, the role cannot even resolve a table name, and PostgreSQL raises exactly permission denied for schema public.
  2. Does the role have SELECT (or other DML) privileges on the specific table? If the schema USAGE is present but the table grant is missing, you get a different error — permission denied for table <name> — not the schema one. So the literal for schema public message specifically means the USAGE grant on the schema is what's missing.

A common trigger is running a migration that adds REVOKE ALL ON SCHEMA public FROM public; to lock down the schema, then forgetting to re‑grant the built‑in roles. A separate, often‑confused failure mode is enabling Row‑Level Security (RLS) without a policy — that does not throw permission denied for schema public. With RLS on and no matching policy a SELECT simply returns zero rows (writes raise new row violates row-level security policy). Same felt symptom — "my client can't read the data" — different cause; see Variant B below.

Under the hood, the Supabase JavaScript SDK is a thin HTTP client for PostgREST. PostgREST opens its database connection as the authenticator role, then runs SET ROLE to switch to the role encoded in your JWT — anon for the public API key, authenticated for a logged‑in user. (service_role is a separate role you only get with the service key, and it bypasses RLS — which is why server‑side code using that key never hits this error.) The query then executes as anon/authenticated, so any privilege those two roles are missing surfaces as the error you see.

The fix#

The fix is a set of GRANT statements that restore the missing privileges. You can run them once in the Supabase SQL editor, or embed them in a migration script if you prefer automation.

sql
-- Grant USAGE on the public schema to the built‑in roles
GRANT USAGE ON SCHEMA public TO anon;
GRANT USAGE ON SCHEMA public TO authenticated;

-- Grant SELECT/INSERT/UPDATE/DELETE on all existing tables
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO anon;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO authenticated;

-- Ensure future tables inherit the same privileges
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO anon;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO authenticated;

That single change addresses the cause because it restores both the ability to resolve objects inside public (USAGE) and the data‑access rights (SELECT, etc.) for the roles that the Supabase client actually runs under.

Step by step#

  1. Open the Supabase dashboard, navigate to SQL editor.
  2. Paste the block above. Adjust the list of privileges if you only need read‑only access (e.g., drop INSERT, UPDATE, DELETE).
  3. Click Run. You should see a success message for each statement.
  4. If you use a migration tool (e.g., supabase db push), add the same statements to a new migration file and deploy.

Verify the fix#

The SQL editor runs as a privileged role, so to reproduce exactly what the client sees you have to impersonate the anon role with SET ROLE, run the query, then reset:

sql
-- In the Supabase SQL editor (or psql), become the anon role and re-run:
set role anon;
select * from public.my_table limit 5;
reset role;

Expected output:

text
 id | name   | created_at
----+--------+----------------------------
  1 | Alice  | 2023-01-01 12:00:00+00
  2 | Bob    | 2023-01-02 13:30:00+00
(2 rows)

If you still see the permission denied for schema public error, double‑check that you ran the grants against the correct project and that you didn’t accidentally create a new role in a later migration.

Variant A — Missing SELECT on a specific table#

Sometimes you grant USAGE on the schema but forget to grant SELECT on a newly created table. The error is slightly different — permission denied for table <name> rather than for schema public — but the cause (a missing grant for the role) is the same. Fix it by adding:

sql
GRANT SELECT ON TABLE public.new_table TO anon;
GRANT SELECT ON TABLE public.new_table TO authenticated;

Variant B — RLS policy blocks access#

If you have RLS enabled on a table, the role may have the right privileges but still be blocked by a policy. Enable RLS first, then add a policy that lets the role read the rows it should see — start permissive only to confirm the policy is the cause, then tighten it before production:

sql
ALTER TABLE public.my_table ENABLE ROW LEVEL SECURITY;

-- Diagnostic only: USING (true) lets every row through. Replace it with a real
-- predicate (e.g. USING (auth.uid() = user_id)) before you ship.
CREATE POLICY allow_read ON public.my_table
  FOR SELECT USING (true);

Why this happens (and how to avoid it next time)#

Supabase’s default security model assumes you either keep the public schema open or explicitly grant the built‑in roles the rights they need. When you start tightening security, the first thing to audit is the USAGE privilege on public and the default privileges for future tables. A quick checklist that saves you from this error:

  1. After any migration that touches schemas, run SELECT * FROM information_schema.role_table_grants WHERE grantee IN ('anon','authenticated'); to verify grants.
  2. Keep a version‑controlled SQL file (e.g., grants.sql) that you run as part of every deployment pipeline.
  3. If you use custom roles, add them to the grant list alongside anon and authenticated.

You can read more about systematic permission audits in our Supabase RLS policy design patterns guide: /guides/supabase-rls-policy-design-patterns. For a deeper dive on why queries become slow when permissions are mis‑configured, see /post/supabase-slow-queries-fix.

Frequently Asked Questions

|

Have more questions? Contact us

Written by

Mahdi Br
Mahdi Br

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.