Encoded Token

HEADERAlgorithm: HS256
{
  "alg": "HS256",
  "typ": "JWT"
}
PAYLOADIssued: Oct 25, 2025, 08:00:00 · Expired
{
  "aud": "authenticated",
  "exp": 1761465600,
  "iat": 1761379200,
  "iss": "https://demoproject.supabase.co/auth/v1",
  "sub": "a8b1c2d3-e4f5-6789-abcd-ef0123456789",
  "email": "user@example.com",
  "phone": "",
  "app_metadata": {
    "provider": "email",
    "providers": [
      "email"
    ]
  },
  "user_metadata": {
    "full_name": "Demo User"
  },
  "role": "authenticated",
  "aal": "aal1",
  "amr": [
    {
      "method": "password",
      "timestamp": 1761379200
    }
  ],
  "session_id": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
  "is_anonymous": false
}
SIGNATUREPresent
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

This tool decodes JWT tokens for inspection only. No signature verification is performed. Never trust unverified tokens in production.

Why Supabase Tokens Are Different

Supabase issues HMAC-SHA256 signed JWTs (HS256) by default — not the asymmetric RS256 most other identity providers use. The signing secret is your project's SUPABASE_JWT_SECRET, which is shared between your Postgres database and your application. Your Postgres Row Level Security (RLS) policies decode the token directly inside SQL using auth.jwt(), which makes Supabase JWTs uniquely intertwined with your database schema.

This pre-loaded sample token is a typical Supabase access token for an authenticated user. Replace it with your own to debug RLS failures, role mismatches, or session inconsistencies.

Supabase-Specific Claims and What They Mean

  • role — Either authenticated (logged-in user), anon (anonymous public access), or service_role (full admin, never exposed to clients). Postgres RLS policies branch on this.
  • aud — Always authenticated for user tokens. RLS uses this implicitly.
  • iss — Format https://<project-ref>.supabase.co/auth/v1. Verify this matches your project before trusting the token.
  • sub — The user's UUID. This is the same value as auth.users.id in your database. RLS policies typically check auth.uid() = user_id.
  • app_metadata — Server-controlled metadata. Users cannot modify this. Use it for plan tier, internal flags, etc.
  • user_metadata — User-controlled metadata. Users can modify this via updateUser(). Never trust it for authorization.
  • aal — Authenticator Assurance Level. aal1 = single-factor, aal2 = MFA verified. Critical for sensitive operations.
  • session_id — Allows server-side session revocation. Storing this lets you invalidate a specific session without rotating the JWT secret.
  • amr — Array of authentication methods used. Includes password, oauth, otp, totp, etc.
  • is_anonymoustrue for guest sessions created via signInAnonymously(). Always check this before granting privileged actions.

Debugging Row Level Security with the Token

"My RLS policy denies a user that should have access." Decode the token, then run SELECT auth.jwt() in the SQL editor as that user. The decoded values must match what your policy expects. The most common bug: a policy checks app_metadata->>'plan' = 'pro' but the field is actually in user_metadata (which users can fake) — or vice versa.

"Anonymous user is being treated as authenticated." Check is_anonymous. RLS policies should explicitly handle this — anonymous tokens still have role=authenticated, which is intentional but easy to miss.

"MFA isn't enforced." Sensitive policies should check aal = 'aal2', not just role = 'authenticated'. Otherwise a single-factor login bypasses MFA.

Related Tools

Common Use Cases

Related Articles