# 401 invalid claim: missing sub claim

> getUser() fails with "invalid claim, missing sub claim" because the token it checked is the anon key, not a user's access token. How to spot it in servers and Edge Functions.

Source: https://supahost.app/kb/invalid-claim-missing-sub · Updated 2026-11-11

## What you see

A call to `supabase.auth.getUser()` fails, most often in server code or an Edge Function:

```
AuthApiError: invalid claim: missing sub claim
```

The response has `error_code: "bad_jwt"`. Depending on the Auth version and how your code reports it, the status is 401 or 403.

## Why it happens

`getUser()` sends a token to Auth and asks which user it belongs to. A user's access token carries that user's id in its `sub` claim. The `anon` and `service_role` keys are also JWTs, but they have no `sub`, because they don't represent a user. Pass one of them to `getUser()` and Auth can't name a user, so it rejects the token.

Common ways a key ends up in place of a user token:

- Nobody is signed in. The client then sends the anon key in the `Authorization` header, and your server or function forwards that header to `getUser()`.
- `supabase.functions.invoke()` ran before sign-in finished, so the request carried the anon key.
- Your code passed the key, not `session.access_token`, to `getUser(token)`.
- A server client was created with the `service_role` key and `getUser()` was called on it without a token.

## How to fix it

1. **Treat it as "not signed in."** In a function or API route, return 401 instead of letting the error bubble up as a 500:

   ```ts
   import { createClient } from "npm:@supabase/supabase-js@2";

   Deno.serve(async (req) => {
     const token = req.headers.get("Authorization")?.replace("Bearer ", "") ?? "";
     const supabase = createClient(
       Deno.env.get("SUPABASE_URL")!,
       Deno.env.get("SUPABASE_ANON_KEY")!,
       { global: { headers: { Authorization: `Bearer ${token}` } } },
     );

     const { data, error } = await supabase.auth.getUser(token);
     if (error || !data.user) {
       return Response.json({ error: "Sign in to save recipes" }, { status: 401 });
     }

     const { data: saved } = await supabase
       .from("saved_recipes")
       .select("recipe_id")
       .eq("user_id", data.user.id);
     return Response.json(saved);
   });
   ```

2. **Only call user-only endpoints with a session.** On the client, check first:

   ```ts
   const { data: { session } } = await supabase.auth.getSession();
   if (!session) return showSignIn();
   await supabase.functions.invoke("saved-recipes");
   ```

3. **Pass the access token, not a key**, when you call `getUser(token)` yourself.

4. **Keep admin and user clients separate.** A client built with `service_role` is for admin work. To find out who is calling, check the caller's token with a client built from the anon key, as in step 1.

## On SupaHost

An Edge Function's **Require JWT** toggle (on by default) checks that the request carries a valid JWT signed with the project's secret. The anon key passes that check. So a request can get through the gateway and still have no user behind it. If a function acts for a user, call `getUser()` in the function as shown above.

## Related

- [Authentication and JWT verification](/docs/functions/auth)
- [Edge Function returns 401](/kb/function-401)
- [API keys](/docs/api/api-keys)
- [Sessions, JWTs and refresh tokens](/docs/auth/sessions)
