Skip to content
Torii docs

JWT templates

A JWT template defines the custom claims Torii puts into a user’s access token. You author it as a free-form JSON document in the dashboard (Project → JWT Templates); string values may contain {{ user.* }} shortcodes that resolve against the signed-in user when a token is minted.

{
"role": "{{ user.public_metadata.role || \"member\" }}",
"email": "{{ user.email }}",
"plan": "{{ user.public_metadata.plan }}"
}

At mint time that becomes, for a given user:

{ "role": "admin", "email": "ada@example.com", "plan": "pro" }

The standard claims Torii always stamps (iss, sub, pid, iat, exp, and when present locale, org_id, org_role, act) are added alongside your template’s claims, so you only author the custom part.

The default template and the session token

Section titled “The default template and the session token”

One template per environment can be marked Default (the star in the dashboard list). Its claims are applied to the session token, the token the SDK holds and getToken() returns. With no default template, session tokens carry only the standard claims.

const { getToken } = useAuth();
const token = await getToken(); // session token: standard claims + the default template

Changing which template is the default takes effect on the user’s next token refresh; it doesn’t rewrite tokens already issued.

A shortcode resolves a single user field. Use it as a whole value (the resolved value keeps its JSON type) or embedded in a string (interpolated as text):

{
"uid": "{{ user.id }}", // whole value
"greeting": "Hi {{ user.first_name }}", // embedded
"team": "{{ user.public_metadata.team || \"none\" }}" // with a default
}
ShortcodeResolves to
{{ user.id }}User id (same as sub)
{{ user.email }}Primary email address
{{ user.email_verified }}true / false
{{ user.name }}Full name
{{ user.first_name }} / {{ user.last_name }}Name parts
{{ user.public_metadata }}The whole public bag (object)
{{ user.public_metadata.<key> }}A key inside the public bag
{{ user.unsafe_metadata }} / {{ user.unsafe_metadata.<key> }}The unsafe bag
  • || "fallback" supplies a value when the shortcode resolves to nothing. Without a fallback, a whole-value shortcode that resolves to nothing omits that claim; an embedded one resolves to an empty string.
  • privateMetadata is never available as a shortcode: it must not reach a token. See user metadata.

Named templates and getToken({ template })

Section titled “Named templates and getToken({ template })”

Beyond the default, you can create additional named templates for tokens you hand to third-party services (Hasura, Supabase, …). Mint one on demand with getToken({ template }):

const { getToken } = useAuth();
// Mints a token from the template named "hasura" for THIS user.
const hasuraToken = await getToken({ template: 'hasura' });
await fetch('https://my-hasura/v1/graphql', {
headers: { Authorization: `Bearer ${hasuraToken}` },
});

A named-template token is returned to you for the call; it never replaces the session token. getToken({ template }) caches each minted token in memory and reuses it until ~30s before it expires (its lifetime is the template’s, see below), then mints a fresh one. It resolves null if no template with that name exists (or the session has ended).

Example “hasura” template projecting the Hasura namespaced claim:

{
"https://hasura.io/jwt/claims": {
"x-hasura-default-role": "{{ user.public_metadata.role || \"user\" }}",
"x-hasura-user-id": "{{ user.id }}"
}
}

A template may set its own lifetime (seconds). A token minted from it expires after that lifetime instead of the environment default, useful for short-lived third-party tokens.

Templates are validated when you save them (and via the dashboard’s Preview, which renders against a sample user):

  • Reserved claims are rejected. You can’t set the standard claims Torii manages: iss, sub, aud, exp, nbf, iat, jti, pid, act, locale, org_id, org_role.
  • Unknown shortcodes are rejected, so a typo can’t silently ship an empty claim.
  • Size budget (~1.2 KB). The worst-case size of the custom claims is checked at save time and rejected if it could overflow the token’s budget. The JWT-eligible metadata bags are capped on write (512 bytes each), so selecting them can’t overflow at mint time.

If you mint tokens on your backend (not via the SDK), the named-template endpoint is bearer-authenticated:

POST /_torii/auth/session/token?template=<name>
Authorization: Bearer <the user's session access token>
→ 200 { "accessToken": "<jwt>", "expiresAt": "<iso8601>" }
→ 404 { "code": "jwt_template_not_found" }