If you used one of the Supabase Clients, such as for JS, Python, or Swift, you may have seen a request that looks something like this:
const { data, error } = await supabase
.from('countries')
.select()These clients are (for the most part) wrappers around REST endpoints. The above query, loosely translates to:
curl 'https://<PROJECT REF>.supabase.co/rest/v1/<TABLE>?select=*' \
-H "apikey: <SERVICE_ROLE KEY | ANON KEY>" \
-H "Authorization: Bearer <SERVICE_ROLE KEY | ANON KEY | AUTH TOKEN>"
The -H stands for headers and when making requests, two headers responsible for authorization are:
- apikey
- Authorization: Bearer
The apikey determines the type of user:
- anon: anonymous
- service_role: admin
The Authorization: Bearer, contains additional information that can introduce another user type: authenticted. The combination of apikey and auth headers determines the user's access privleges:
Here are all the combinations:
anon user:
apikey: ANON KEYAuthorization: Bearer ANON KEY
authenticated user:
apikey: ANON KEY | SERVICE_ROLE KEYAuthorization: Bearer <AUTH_TOKEN>
service_role:
apikey: SERVICE_ROLE KEYAuthorization: Bearer: SERVICE_ROLE KEY
In general, if you need to use both the service_role and authenticate users, you should create a seperate client exclusively for the service_role.
In some frameworks, especially meta-frameworks (next, sveltekit, nuxt), if this isn't done properly, the service_role client's Authorization: Bearer can be overwritten with a user's JWT session. This will downgrade the service_role's permission to that of an authenticated user.
To test if you're making requests with the service_role as the apikey, but the Authorization Bearer as the anon or authenticated user, you can run this query in the log explorer:
select
cast(timestamp as datetime) as timestamp,
cf_connecting_ip AS ip,
x_client_info as client_library,
auth_user as user_id,
path,
url as full_url,
payload.role AS client_library_token,
bearer_payload.role AS session_token
from edge_logs
cross join unnest(metadata) as m
cross join unnest(m.request) as r
cross join unnest(r.headers) as h
cross join unnest(r.sb) as s
cross join unnest(s.jwt) as jwt
cross join unnest(jwt.apikey) as apikey
cross join unnest(apikey.payload) as payload
cross join unnest(jwt.authorization) as authorization
cross join unnest(authorization.payload) as bearer_payload
where
(payload.role = 'service_role' OR bearer_payload.role = 'service_role')
AND
(payload.role <> bearer_payload.role)
order by
timestamp desc
limit 100;Note, logs are collected in batches, so it may take up to 5 minutes before new ones are avaiable
When using the log explorer, please set an appropriate time range:

Logs returned by queries may be difficult to read in table format. A row can be double-clicked to expand the results into more readable JSON:
