Embed a tier custom claim in the access token so your application can decide which features to show or hide immediately after decoding the token — no additional API call, no round-trip to a user store.
When to use it
Section titled “When to use it”Any product with multiple subscription tiers benefits from this pattern. When your application gates a feature on the user's plan, it needs that plan value somewhere. Fetching it from your billing system on every request is slow and adds operational coupling. Putting it in the token means the application has it the moment authentication completes. The pattern generalises to any property your application needs but does not want to look up on every request: region, department, account status, feature-flag overrides — any value that changes infrequently and lives in app_metadata.
Cymmetri, for example, uses this recipe to gate their workspace collaboration features. When a user on the free tier logs in, the access token carries tier: "free" and the frontend immediately renders a read-only view. Enterprise accounts see tier: "enterprise" and the editing controls appear — no API call, no loading state.
What you will build
Section titled “What you will build”A custom action named inject-tier-claim triggered on Login. The action reads event.user.app_metadata.tier, defaults to 'free' if the field is absent, and calls api.session.setCustomClaim to embed the value in the access token. You will attach this action to the Post-Login stage of the Login flow, after the Issue Session block has run.
The complete configuration is:
- A custom action named
inject-tier-claim, trigger Login. - A Run Custom Action block in the Post-Login stage of the Login flow, positioned after Issue Session.
- The Run Custom Action block configured to run
inject-tier-claim.
Optionally, you can attach the same action to the Token Refresh flow's Pre-Issue stage so the claim is refreshed when a user upgrades their plan mid-session.
Configure it
Section titled “Configure it”Step 1 — Create the custom action
Section titled “Step 1 — Create the custom action”Open Custom Actions in the left navigation and click + New Action.
Set:
- Name:
inject-tier-claim - Trigger: Login
Leave the other settings at their defaults and click Save. The code editor opens.
Step 2 — Paste the code
Section titled “Step 2 — Paste the code”Replace the default function body with the code in the Code section below. Click Save.
Step 3 — Enable the action
Section titled “Step 3 — Enable the action”Toggle the action to Enabled in the action detail header. A disabled action is silently skipped even when a block calls it — confirm the toggle is on before testing.
Step 4 — Add the block to the Login flow
Section titled “Step 4 — Add the block to the Login flow”Navigate to Flows, click Login, then click Edit.
In the flow editor, expand the Post-Login stage. Drag a Run Custom Action block from the block palette into the Post-Login stage. Place it after the Issue Session block — the claim must be written after the session exists.
In the block's configuration panel, select inject-tier-claim from the Action dropdown. Click Save.
Step 5 — Publish the flow
Section titled “Step 5 — Publish the flow”Click Publish in the flow editor toolbar. The change is live for the next login attempt.
Optional — Refresh-flow attachment
Section titled “Optional — Refresh-flow attachment”If your product allows users to upgrade their subscription without signing out, you should also attach this action to the Token Refresh flow. When a refresh token is exchanged for a new access token, the action runs again and reads the current app_metadata.tier value — so an upgraded user gets the new tier on their next refresh without needing to log out and back in.
Navigate to Flows, click Token Refresh, then click Edit. Add a Run Custom Action block to the Pre-Issue stage, configured to run inject-tier-claim. Publish the flow.
The code
Section titled “The code”async function injectTierClaim(event, api) { // Read the tier from admin-controlled metadata. // Defaults to 'free' if the field is absent, which covers: // - users who signed up before app_metadata.tier was populated // - users whose tier was not set at signup const tier = event.user?.app_metadata?.tier ?? 'free';
// Optional: embed additional claims if your application needs them. // Keep claims small — a handful of strings is well within the token budget. const region = event.user?.app_metadata?.region ?? null; const department = event.user?.app_metadata?.department ?? null;
// Write the tier claim into the access token. // The claim name must not conflict with standard JWT registered claims // (sub, iss, aud, exp, nbf, iat, jti). api.session.setCustomClaim('tier', tier);
if (region) { api.session.setCustomClaim('region', region); } if (department) { api.session.setCustomClaim('department', department); }
api.log('info', `Claims injected — tier: ${tier}, region: ${region ?? 'unset'}, department: ${department ?? 'unset'}`);}Test it
Section titled “Test it”Open the action in Custom Actions, click Test, and select the Logged-in user preset.
In the Event JSON editor, locate the user.app_metadata object and add "tier": "enterprise". Click Run.
In the API calls tab of the result, you should see:
api.session.setCustomClaimcalled with('tier', 'enterprise').api.logcalled with a message containingtier: enterprise.
Now remove the tier field from app_metadata entirely and run again. You should see api.session.setCustomClaim called with ('tier', 'free') — confirming the default path works.
Verify on a live flow
Section titled “Verify on a live flow”Sign in to your tenant as a test user. Copy the access token from the network tab or from your application's session storage. Paste it into jwt.io and inspect the payload.
You should see a tier field in the decoded payload alongside the standard claims. The exact claim namespace prefix depends on your tenant's token configuration — confirm it in Flows → Login → Token settings if you do not see the bare tier key.
Custom claims are visible to anyone who holds the access token. JWTs are base64url-encoded, not encrypted. Do not put values that need to stay private — billing identifiers, internal cost codes, security classifications — in a custom claim.
Cautions
Section titled “Cautions”Claims are stamped at issuance. The access token captures the tier value at the moment the user logs in. If a user upgrades their subscription after signing in, their active token still carries the old tier until it expires. Add the optional refresh-flow attachment above if your product needs the claim to update without a full sign-out. Even then, the claim updates on the next token refresh, not instantaneously.
Keep claims small. Access tokens have a practical size budget. A handful of string claims is fine. Embedding deeply nested objects, large arrays, or long lists of permissions inflates the token on every request. If you find yourself writing many claims, consider whether a back-channel call is the better design for those specific values.
Claim names must not shadow standard fields. Avoid sub, iss, aud, exp, nbf, iat, and jti. If your platform adds a claim namespace prefix automatically, check the decoded token to confirm where your claim lands before building application code against it.
app_metadata is admin-owned. Nothing in this action writes to app_metadata — it only reads from it. The Tag corporate vs self-serve recipe and the Tag tier at signup recipe are the recommended patterns for populating app_metadata.tier in the first place. Run that recipe first if you have not already.