Menu

Developer documentation

JWT claim templates

/docs/jwt-templates

Status: shipped 2026-08-04.

Signet's session-protected GET {base_path}/token endpoint mints EdDSA JWTs with the key published at {base_path}/jwks. With no query parameter it preserves the better-auth JWT-plugin shape: the public user object plus server-owned registered claims and a 15-minute lifetime.

Named templates are an opt-in Signet extension for relying parties that need a smaller or different claim set:

[jwt]
default_expires_in = 900

[[jwt.templates]]
name = "supabase"
audience = "authenticated"
expires_in = 3600

[jwt.templates.claims]
role = "authenticated"
email = "{{user.email}}"
session_id = "{{session.id}}"
app_metadata = { provider = "signet", profile = "{{user.publicMetadata}}" }

Request it with the same live session cookie used by the rest of the auth API:

const response = await fetch("https://auth.example.com/api/auth/token?template=supabase", {
  credentials: "include",
})
const { token } = await response.json()

An unknown template returns HTTP 400 JWT_TEMPLATE_NOT_FOUND; a caller without a live session still receives 401 first. Template names are 1–64 ASCII letters/digits plus ., _, or -, must start with a letter/digit, and are unique. At most 32 templates may be configured. Each lifetime is 60–86,400 seconds and each configured claim tree is limited to 8 KiB and eight nesting levels.

Claim ownership

A named template is an allow-list. It starts empty, resolves only the configured claims, and then Signet adds these registered claims:

ClaimNamed-template value
issthe instance's full auth issuer (server.base_url + server.base_path)
subauthenticated user id
iatcurrent Unix time
expiat + expires_in
audtemplate audience, or server.base_url when omitted

Putting iss, sub, iat, exp, or aud inside claims stops boot. audience is the only declarative registered-claim override. Signet owns the issuer/subject/time facts so metadata cannot turn a token into another principal, issuer, or lifetime.

Claim values may be strings, booleans, numbers, arrays, objects, or null. A string which is exactly one placeholder is replaced while preserving its JSON type; interpolation inside a larger string is rejected. If an allowed optional field is absent, its value is JSON null.

Available user placeholders:

{{user.id}}  {{user.email}}  {{user.name}}  {{user.image}}
{{user.emailVerified}}  {{user.createdAt}}  {{user.updatedAt}}
{{user.role}}  {{user.banned}}  {{user.banReason}}  {{user.banExpires}}
{{user.username}}  {{user.displayUsername}}  {{user.isAnonymous}}
{{user.publicMetadata}}  {{user.unsafeMetadata}}

Available session placeholders:

{{session.id}}  {{session.userId}}  {{session.expiresAt}}
{{session.createdAt}}  {{session.updatedAt}}  {{session.ipAddress}}
{{session.userAgent}}  {{session.activeOrganizationId}}  {{session.impersonatedBy}}

There is deliberately no {{user.privateMetadata}} or {{session.token}}. Both are rejected at boot rather than silently rendered. Templates can copy public/unsafe metadata, so authorization policy should prefer operator-owned static claims or data from a trusted application database; do not treat user-writable unsafe metadata as an entitlement.

Relying-party boundaries

The template changes claims; it does not configure the relying party. Verify the compact token's signature against {base_path}/jwks, require EdDSA, bind the expected iss and aud, validate exp/iat, and make authorization decisions from an explicit claim contract. Key rotation and cache behavior remain relying-party operational concerns.

The supabase example is a Supabase-style role/audience shape, not a promise that a hosted Supabase project accepts Signet as a first-class provider. Supabase's current third-party-auth catalog is provider-specific even though its verifier requires asymmetric JWTs, OIDC discovery, and a kid; confirm the current project integration before deployment. Signet emits asymmetric EdDSA tokens with a kid and matching discovery issuer, but claim shape alone cannot enroll an issuer.

Firebase custom tokens are not supported by this feature. Firebase requires RS256 with a Google service-account issuer and subject, a fixed Google audience, a uid, and at most a one-hour life. Signet deliberately signs EdDSA as its own issuer and will not let a template impersonate a Google service account. Use the Firebase Admin SDK/service-account signer when Firebase custom tokens are required.

Primary references retrieved 2026-08-04:

  • Supabase third-party auth overview: https://supabase.com/docs/guides/auth/third-party/overview
  • Firebase custom-token contract: https://firebase.google.com/docs/auth/admin/create-custom-tokens

Enter to open · Esc to close