# 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.

Source: https://supahost.app/docs/database/roles · Updated 2026-11-11

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](/docs/api/api-keys).

## Grants and RLS work together

Two checks run before a row is returned:

1. **Grants** decide whether a role may touch a table at all (`select`, `insert`, `update`, `delete`).
2. **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.

```sql
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:

```sql
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](/kb/permission-denied-42501) 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:

```sql
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](/kb/revoke-function-execute) and [security definer functions and RLS](/kb/security-definer-functions).

## See what a role can do

```sql
-- 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_reporting` rather than `reporting`.
- **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-one` can'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:

```sql
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](/docs/platform/ssh)). 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](/docs/auth/custom-claims-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](/docs/database/connections-and-timeouts).

To remove a role you created, see [How to delete a Postgres role](/kb/delete-role).

## Related

- [Row Level Security](/docs/database/row-level-security)
- [Securing your API](/docs/api/securing-your-api)
- [service_role client still gets RLS errors](/kb/service-role-rls-errors)
- Postgres reference: [Database roles](https://www.postgresql.org/docs/17/user-manag.html)
