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 → Settings → JWT templates); string values may contain {{ user.* }}
shortcodes that resolve against the signed-in user and the external identity bound
to the current session 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, iat, exp, and when
present locale, act, identity_id, and the active-organization block o) 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 templateChanging which template is the default takes effect on the user’s next token refresh; it doesn’t rewrite tokens already issued.
Shortcodes
Section titled “Shortcodes”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}| Shortcode | Resolves 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 |
{{ identity.issuer }} |
Bound external identity issuer |
{{ identity.subject }} |
Bound external identity subject |
{{ identity.claims.<provider>.<key> }} |
An allowlisted claim stored for the bound provider identity |
|| "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.privateMetadatais never available as a shortcode: it must not reach a token. See user metadata.- Identity claims are provider-qualified. For example,
{{ identity.claims.google.email }}and{{ identity.claims.github.email }}are separate expressions. - Identity shortcodes resolve only for the external identity bound to the current session. With no bound identity, whole-value expressions are omitted unless a fallback is supplied. Preview cannot select a live identity and reports these expressions as unresolved.
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 }}" }}Token lifetime
Section titled “Token lifetime”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.
Validation, reserved claims, and size
Section titled “Validation, reserved claims, and size”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,act,locale,feature_flags,fea,o,identity_id. - Unknown shortcodes are rejected, so a typo can’t silently ship an empty claim.
- Size is advisory, not enforced. The dashboard’s Preview warns when the
rendered custom claims exceed 1229 bytes, a cookie-safe size. Saving is
never blocked on size, and a large claim is minted as-is: a session token is
sent on every request, and the hard ceiling is the 16 KB
Authorizationheader limit, not the warning. Since a JWT-eligible metadata bag can hold up to 8 KB (see user metadata), projecting a whole bag can exceed the recommendation — select a sub-key when the bag is large.
Server-minted tokens
Section titled “Server-minted tokens”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" }