Roles and permissions
The Postgres roles in a SupaHost project (postgres, anon, authenticated, service_role and the service roles), how grants and RLS work together, and how to add your own roles safely.
Postgres decides what each connection may do based on its role. In a Supabase project, the Data API, Auth and Storage each connect with their own role, and every request from your app runs as one of three API roles. Knowing which role runs a query explains most "permission denied" errors and most surprises with Row Level Security.
The roles you'll meet
| Role | Who uses it | Notes |
|---|---|---|
postgres |
You: Studio, the SQL editor, SSH and the tunnel on Business | Owns your tables and functions. Can create objects in public. |
anon |
Data API requests sent with the anon key and no signed-in user |
Subject to RLS. |
authenticated |
Data API requests with a signed-in user's JWT | Subject to RLS. auth.uid() returns the user's id. |
service_role |
Requests sent with the service_role key |
Bypasses RLS. Server-side only. |
authenticator |
The Data API's login role | Switches to anon, authenticated or service_role per request based on the JWT. |
supabase_auth_admin |
The Auth service | Owns the auth schema. |
supabase_storage_admin |
The Storage service | Owns the storage schema. |
supabase_admin |
SupaHost's agent | Platform maintenance. Don't alter or use it. |
sh_readonly |
The MCP server in read-only mode | Can read all data, can't write. |
The API role comes from the role claim of the JWT the request carries. The anon and service_role keys are themselves JWTs with those claims, which is why sending the service_role key skips RLS. Keep that key on the server: see API keys.
Grants and RLS work together
Two checks run before a row is returned:
- Grants decide whether a role may touch a table at all (
select,insert,update,delete). - RLS policies decide which rows it may see or change.
Supabase's defaults grant table privileges in public to anon, authenticated and service_role, so new tables are reachable through the Data API straight away. That's why enabling RLS on every table in public matters: with RLS on and no policies, nobody except service_role and the table owner gets any rows.
alter table public.invoices enable row level security;
create policy "Owners read their invoices"
on public.invoices for select
to authenticated
using ((select auth.uid()) = user_id);
Grants are the coarser tool. Use them to shut a role out entirely. For example, a ledger table that only server code should write:
revoke insert, update, delete on public.ledger_entries from anon, authenticated;
A missing grant shows up as error 42501. See "permission denied for table" (42501) errors for the common causes.
Functions and roles
Database functions run as the caller by default (security invoker), so RLS applies inside them. A security definer function runs as its owner, usually postgres, which bypasses RLS. Use definer functions sparingly, set search_path = '', and decide who may call them:
revoke execute on function public.close_month(date) from public, anon, authenticated;
grant execute on function public.close_month(date) to service_role;
More in How to revoke execute on a Postgres function and security definer functions and RLS.
See what a role can do
-- Table privileges for the API roles in public
select grantee, table_name, string_agg(privilege_type, ', ') as privileges
from information_schema.role_table_grants
where table_schema = 'public'
and grantee in ('anon', 'authenticated', 'service_role')
group by grantee, table_name
order by table_name, grantee;
-- Role attributes and per-role settings
select rolname, rolsuper, rolbypassrls, rolcanlogin, rolconfig
from pg_roles
where rolname in ('postgres', 'anon', 'authenticated', 'service_role', 'authenticator');
You can also check access for a specific role with has_table_privilege('anon', 'public.invoices', 'select').
Adding your own roles
Postgres roles belong to the server, not to a database. On SupaHost each instance runs one Postgres server and every project is a separate database on it, so a role you create in one project exists for all projects on that instance. Two practical consequences:
- Name roles after the project to avoid clashes, for example
app_one_reportingrather thanreporting. - Privileges stay per database. Grants on tables, schemas and functions apply only in the database where you run them, so a role created in
app-onecan't read another project's tables unless someone grants it access there.
A read-only role for a reporting tool on Business, connecting over the SSH tunnel:
create role app_one_reporting with login password 'use-a-long-random-password';
grant usage on schema public to app_one_reporting;
grant select on all tables in schema public to app_one_reporting;
alter default privileges in schema public grant select on tables to app_one_reporting;
Roles that log in directly only matter where direct connections are possible, which is Business and Enterprise through the tunnel (SSH access and direct Postgres). On other plans, outside code reaches the database through the Data API with the API roles above.
For app-level roles like "admin" and "member", don't create Postgres roles per user. Store the role in a table or a custom JWT claim and check it in policies: see Roles, custom claims and RBAC.
Don't change the platform roles
Leave supabase_admin, authenticator, supabase_auth_admin, supabase_storage_admin and sh_readonly as they are. Changing their passwords, attributes or memberships can stop Auth, Storage, the Data API or the MCP server from working for that project. It's fine to grant privileges on your own objects to anon, authenticated and service_role, and to set per-role timeouts as described in Connections and timeouts.
To remove a role you created, see How to delete a Postgres role.
Related
- Row Level Security
- Securing your API
- service_role client still gets RLS errors
- Postgres reference: Database roles