How to Decode a Supabase JWT: User ID, Role, and Session Claims
- Supabase JWT structure: what claims every session token contains
- How the role claim drives Row Level Security (RLS) policies
- Where user_metadata and app_metadata appear in the token
- Reading Supabase tokens in JavaScript and server-side code
Table of Contents
Supabase uses JWTs for session management and Row Level Security. The token your frontend holds controls what data a user can read and write via RLS policies. Decoding it reveals the user ID, role, and metadata — paste it into the decoder above to inspect it.
What a Decoded Supabase JWT Contains
{
"iss": "https://YOUR-PROJECT.supabase.co/auth/v1",
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"aud": "authenticated",
"exp": 1700003600,
"iat": 1700000000,
"email": "[email protected]",
"phone": "",
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"user_metadata": {
"name": "Jane Smith"
},
"role": "authenticated",
"aal": "aal1",
"amr": [{ "method": "password", "timestamp": 1700000000 }],
"session_id": "uuid-here"
}
The sub is the Supabase user UUID — this is the user's permanent ID in the auth.users table. The role claim is either authenticated (logged-in user) or anon (anonymous/unauthenticated).
How the role Claim Controls Row Level Security
Supabase's RLS policies use the JWT role claim directly. When you write a policy like:
-- Only authenticated users can read
CREATE POLICY "Users can read own data"
ON profiles FOR SELECT
USING (auth.uid() = id);
Supabase extracts auth.uid() from the JWT's sub claim and auth.role() from the role claim. If your token has "role": "authenticated", RLS policies that check for authenticated access will pass.
If you are testing RLS and queries return no data, decode your JWT to confirm the role claim is authenticated and the sub matches the user ID you expect.
Adding Custom Claims to Supabase JWTs
Supabase lets you add custom claims via a PostgreSQL hook or a Supabase Edge Function. A common pattern is adding a user_role or org_id for multi-tenant apps:
-- PostgreSQL function (custom access token hook)
CREATE OR REPLACE FUNCTION custom_access_token_hook(event jsonb)
RETURNS jsonb AS $$
DECLARE claims jsonb;
BEGIN
claims := event -> 'claims';
claims := jsonb_set(claims, '{user_role}',
(SELECT to_jsonb(role) FROM user_roles WHERE user_id = (event->>'user_id')::uuid)
);
RETURN jsonb_set(event, '{claims}', claims);
END;
$$ LANGUAGE plpgsql;
After wiring the hook in the Supabase dashboard, the decoded token will include a top-level user_role field. Paste a fresh token into the decoder to confirm the hook is working.
Read Supabase JWT Claims in JavaScript
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(url, anonKey);
// Get the current session and decode the token
const { data: { session } } = await supabase.auth.getSession();
if (session) {
const token = session.access_token;
// Decode payload (no library needed)
function decodePayload(t) {
const p = t.split('.')[1];
return JSON.parse(atob(p.replace(/-/g, '+').replace(/_/g, '/') + '=='));
}
const payload = decodePayload(token);
console.log('User ID:', payload.sub);
console.log('Role:', payload.role);
console.log('Email:', payload.email);
console.log('Custom:', payload.user_role); // if hook is set
}
Supabase also exposes session.user directly, which contains most of this info without decoding the JWT — but decoding the raw token is useful for confirming custom claims are present.
Inspect Your Supabase Token
Paste your Supabase access token above — verify role, sub, and custom claims before debugging RLS issues.
Open Free JWT DecoderFrequently Asked Questions
How long do Supabase JWTs last?
By default, Supabase access tokens expire after 1 hour. The refresh token lasts for a longer period (configurable in your project settings). The Supabase client SDK handles auto-refresh silently.
Why does my Supabase RLS policy block authenticated users?
Decode the JWT and check that the role claim is "authenticated" and the sub matches the user ID in your table. Also verify you are passing the JWT in the request — if using service role key or no auth header, Supabase may bypass or apply different policies.
What is the aal claim in a Supabase JWT?
aal stands for Authentication Assurance Level. aal1 means single-factor authentication (password or social login). aal2 means MFA was used. Supabase RLS policies can require aal2 for sensitive operations.

