Use Supabase Auth + Custom JWT Claims for Role‑Based Access Control in Serverless Apps
Learn how to add OAuth login with Google or GitHub, embed role claims in the JWT via Supabase Admin API, and read those claims in Edge Functions for fine‑grained access control.
17 Sept 2025, 06:46 UTC

Problem: How to add role‑based protection without a separate identity provider?
In many serverless projects, you want a quick way for users to sign in with a familiar provider (Google, GitHub) and then enforce roles like admin or editor inside your Edge Functions. Building a custom auth server is overkill, but Supabase Auth alone only gives you a generic JWT. The question is: Can we embed roles into that token and read them in our functions without extra infrastructure?
Thesis: Supabase Auth + Custom Claims gives you a single‑source identity with built‑in role support.
Supabase Auth natively supports OAuth providers and, through the Admin API, lets you attach arbitrary JSON claims to the JWT. Edge Functions can read those claims directly from event.context.user, making role checks fast and stateless. The trade‑off is that you must keep the token refreshed after role changes and avoid putting sensitive data in the claim payload.
1. Enable an OAuth Provider in the Supabase Dashboard
Navigate to Authentication → Settings → External OAuth Providers and toggle Google or GitHub. You’ll need to supply the client ID and secret from the provider’s developer console.
After enabling, the provider is available for sign‑in via the client SDK:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key')
// Sign in with Google
const { user, session, error } = await supabase.auth.signInWithOAuth({
provider: 'google'
})
Run this code in a browser console or a frontend component. The user is now authenticated and receives a JWT that contains only the default claims.
2. Add a Custom Claim for Role via the Admin API
Supabase’s Admin API exposes a set_custom_claims RPC that accepts a user ID and a JSON object. The RPC is available on the auth.users table. Example request:
POST https://xyzcompany.supabase.co/rest/v1/rpc/set_custom_claims
Content-Type: application/json
apikey: SERVICE_ROLE_KEY
{
"id": "user-uuid",
"claims": { "role": "admin" }
}
Replace user-uuid with the target user’s id from auth.users and SERVICE_ROLE_KEY with a service‑role key that has rpc privileges. The response returns the updated user record; the JWT issued after this call will now include role: "admin" in its payload.
Important: supabase-js v2+ automatically refreshes tokens on login or token rotation, so the claim will propagate to the client without manual intervention.
3. Read the Claim in an Edge Function
Supabase Edge Functions expose the decoded JWT via event.context.user. A minimal function that returns the role looks like this:
export async function POST(req, { params, context }) {
const role = context.user?.role ?? 'guest'
return new Response(JSON.stringify({ role }), {
headers: { 'Content-Type': 'application/json' }
})
}
Deploy the function (e.g., supabase functions deploy role-check) and invoke it with an authenticated request:
curl -H "Authorization: Bearer <access_token>" \
https://xyzcompany.supabase.co/functions/v1/role-check
The response should contain the role you set via the Admin API. If the user’s role changes, you must re‑authenticate or force a token refresh to see the new claim.
4. Enforce Role Checks in Your Application
With the role available in the request context, you can gate access to resources directly in the Edge Function:
export async function GET(req, { context }) {
if (context.user?.role !== 'admin') {
return new Response('Forbidden', { status: 403 })
}
// … serve protected data
}
This pattern keeps your functions stateless and removes the need for a separate RBAC service.
Trade‑Offs and Limitations
- Token Refresh: Role changes only propagate after the client obtains a new JWT. If you change a user’s role via the Admin API, the old token will still contain the stale claim until the next refresh or re‑login.
- Sensitive Data: JWTs are base64‑encoded but not encrypted. Avoid putting secrets or personally identifying information in custom claims unless you add your own encryption layer.
- SDK Version: Custom claims are respected only by
supabase-jsv2+. Earlier versions ignore therolefield. - Rate Limits: The
set_custom_claimsRPC is subject to Supabase’s REST API rate limits. For bulk role updates, consider batching or using a serverless function with a service‑role key.
Actionable Next Steps
- Enable Google or GitHub OAuth in your Supabase project.
- Use the Admin API to assign roles to existing users.
- Deploy an Edge Function that reads
event.context.user.roleand returns or enforces it. - In your client, check
session.user.app_metadata?.roleafter login to confirm the claim is present. - Implement a small UI button that triggers
supabase.auth.signOut()followed bysupabase.auth.signInWithOAuth()to force a token refresh after role changes.
By following these steps, you can quickly add role‑based access control to a serverless application without spinning up a separate identity provider, keeping your stack lean and your codebase simple.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.