User metadata
Every user carries three free-form JSON metadata bags. They differ only in who can read and write them: pick the bag that matches the trust boundary of the data you are storing.
| Bag | Read in SDK | Write in SDK | Read on backend | Write on backend | In a JWT? | Use for |
|---|---|---|---|---|---|---|
publicMetadata | ✓ | ✗ | ✓ | ✓ | optional | Plan tier, role; the user may see it, but only your server may set it. |
privateMetadata | ✗ | ✗ | ✓ | ✓ | ✗ | Stripe customer id, internal flags; server-only secrets. |
unsafeMetadata | ✓ | ✓ | ✓ | ✓ | optional | UI preferences, onboarding step; the user may change it from the browser. |
Size limits
Section titled “Size limits”Each write is size-checked and rejected with 400 if it exceeds the cap:
publicMetadataandunsafeMetadata: 512 bytes each (they are eligible to be projected into a JWT, which has a strict size budget).privateMetadata: 4096 bytes (never enters a JWT).
Store anything larger in your own database and keep only a reference here.
Reading metadata in the SDK
Section titled “Reading metadata in the SDK”publicMetadata and unsafeMetadata are part of the /me profile. Read them
with useUserProfile:
import { useUserProfile } from '@torii-js/torii-react';
function PlanBadge() { const { profile } = useUserProfile(); const plan = profile?.publicMetadata.plan ?? 'free'; return <span>{String(plan)}</span>;}privateMetadata is intentionally absent from the SDK: it is only returned by
the backend API.
Writing unsafe metadata from the SDK
Section titled “Writing unsafe metadata from the SDK”unsafeMetadata is the only bag the end-user can write. Updates deep-merge:
send only the keys you want to change, and set a key to null to remove it.
const { updateProfile } = useUserProfile();
// Merges into the existing bag, other keys are preserved.await updateProfile({ unsafeMetadata: { theme: 'dark' } });
// Remove a key.await updateProfile({ unsafeMetadata: { theme: null } });Writing public and private metadata from your backend
Section titled “Writing public and private metadata from your backend”Use the secret-key backend API. Each bag is independent: omit a bag to leave it
unchanged, or send an object to deep-merge into it (a key set to null is removed).
curl -X PATCH https://api.torii.so/api/server/v1/users/$USER_ID/metadata \ -H "Authorization: Bearer $TORII_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "publicMetadata": { "plan": "pro", "role": "admin" }, "privateMetadata": { "stripeId": "cus_123" } }'You can also seed any of the three bags when creating a user via
POST /api/server/v1/users.
Linking a user to your backend
Section titled “Linking a user to your backend”$USER_ID above is the same identifier the SDK exposes as user.id. One id
spans all three surfaces, so it is the key your backend joins on:
useUser().user.idin the SDK,- the
subclaim of every access token, - the
{userId}path segment on/api/server/v1/users/{userId}.
import { useUser } from '@torii-js/torii-react';
function Profile() { const { user } = useUser(); // user.id === the JWT `sub` === the BAPI user id your backend stores. return <code>{user?.id}</code>;}Writing private metadata right after sign-up
Section titled “Writing private metadata right after sign-up”A common pattern is to enrich a new user from your backend the moment they sign
up (create a Stripe customer, write the id into privateMetadata, …). Wire it
with the SDK’s onSignupSuccess callback, then let your backend do the
privileged write with its secret key:
const { getToken } = useAuth();
<SignUp onSignupSuccess={async () => { const token = await getToken(); // the user's session JWT await fetch('https://api.your-app.com/provision', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, }); }}/>end-user SDK (browser) your backend Torii | sign up ------> onSignupSuccess() | getToken() ---------> POST /provision (Bearer <session JWT>) | | verify JWT ----------> GET /.well-known/jwks.json | | userId = sub | | PATCH /api/server/v1/users/{sub}/metadata | | { "privateMetadata": { "stripeId": "cus_…" } } (sk_…) | <----------------------- 200 okPutting metadata in a JWT
Section titled “Putting metadata in a JWT”A JWT template projects metadata into the access token via shortcodes. Reference a whole bag or a single key:
{ "plan": "{{ user.public_metadata.plan }}", "theme": "{{ user.unsafe_metadata.theme || \"light\" }}"}privateMetadata is deliberately not available as a shortcode. The template is
size-checked when you save it; because the JWT-eligible bags are capped on write,
they can’t overflow the ~1.2 KB custom-claims budget at mint time. A shortcode
that resolves to nothing omits its claim unless you give it a || "default".