☰ Docs menu · Custom claims
Custom claims
Put facts about a user into every token: a plan, a role, the teams they belong to. Your server sets them with the server secret, Sync security rules read them, and the app reads them to show the right screens. The Firebase custom claims model.
A user paid on your website, so their app should unlock the pro lessons. A shop marks some users as staff. A B2B app knows which teams a user belongs to. Custom claims carry such facts inside the user's token, signed, so every Abrova product can trust them and the app can't change them. If you know Firebase custom claims, this works the same way.
How it works#
- Your server sets the claims for a user, with the project's server secret:
setCustomUserClaims(userId, { plan: 'pro' }). - Every new token of that user carries them, at the top level, next to
subandexp. The user gets the new token at the next sign-in or renewal, within an hour, or at once when the app asks for a fresh one. - Sync security rules read them as
auth.token.plan, and the app reads them withgetTokenResult()to show the right screens.
How the user paid, or why they are staff, stays your business: Abrova only carries the result.
Step 1: Set claims on your server#
With the Admin SDK for Node.js:
import { initAdmin } from '@abrova/auth-admin';
const auth = initAdmin(); // reads ABROVA_PROJECT_ID, ABROVA_API_KEY, ABROVA_SERVER_SECRET
// After your server confirmed the payment:
await auth.setCustomUserClaims(userId, { plan: 'pro', levels: ['L1', 'L2'] });Or with any language, as one HTTPS request:
curl -X PUT "https://auth-users.abrova.ir/v1/projects/$PROJECT_ID/users/$USER_ID/custom-claims" \
-H "x-api-key: $ABROVA_API_KEY" \
-H "x-server-secret: $ABROVA_SERVER_SECRET" \
-H "content-type: application/json" \
-d '{"claims": {"plan": "pro", "levels": ["L1", "L2"]}}'userId is the Abrova Auth user id (ru_...), the one your app gets after sign-in.
Setting claims needs the server secret (ab_srv_...). Never put it in an app or a web page: anyone could give themselves any claim. An app can only read claims, never set them.
Setting claims replaces them#
Each call replaces all of the user's claims, as in Firebase. To add one claim and keep the others, read them first:
const user = await auth.getUser(userId);
await auth.setCustomUserClaims(userId, { ...user.customClaims, beta: true });To remove all claims, send null (or {}):
await auth.setCustomUserClaims(userId, null);
Step 2: Use them in security rules#
Sync rules read every claim as auth.token.<name>, nested values included. A text claim named role is also auth.role:
{
"rules": {
"lessons": {
"$id": {
".read": "auth.token.plan === 'pro' || data.free === true"
}
},
"teams": {
"$team": {
".write": "auth.token.teams.includes($team) && auth.token.team.role === 'editor'"
}
}
}
}A user without the claim fails the check. See Sync security rules.
Step 3: Read them in the app#
getTokenResult() returns the token with its decoded fields, like Firebase's getIdTokenResult(). Use it to show the right screens (a "Pro" badge, unlocked lessons). Abrova Auth SDKs 0.2.0 and later:
// Web, React Native const result = await auth.getTokenResult(); if (result?.claims.plan === 'pro') showProBadge();
// Flutter final result = await auth.getTokenResult(); if (result?.claims['plan'] == 'pro') showProBadge();
// iOS
let result = try await AbrovaAuth.shared.getTokenResult()
if result?.claims["plan"] as? String == "pro" { showProBadge() }// Android
val result = AbrovaAuth.shared.getTokenResult()
if (result?.claims?.get("plan") == "pro") showProBadge()| Field | What it is |
|---|---|
| token | The access token itself, the same one getToken() gives. |
| claims | Every field of the token: sub (the user id), pid, iat, exp, auth_time, amr, and your custom claims. |
| expirationTime | When this token expires (iOS: expirationDate, Android: expirationTimestamp). |
| issuedAtTime | When this token was issued (iOS: issuedAtDate, Android: issuedAtTimestamp). |
| authTime | When the user signed in. A renewal does not change it (iOS: authDate, Android: authTimestamp). |
| signInProvider | How the user signed in: anonymous, apple, google, password, magic_link or telegram. |
The app can read its own token, but deciding access in the app alone is never safe. Show screens with getTokenResult(); protect data with security rules, which check the signed token on the server.
Get new claims at once#
A changed claim reaches the app with its next token, within an hour. To get it at once, for example on a "Check my purchase" button or when the app opens, ask for a fresh token:
await auth.getTokenResult({ forceRefresh: true });Then tell Abrova Sync to use it: sync.refreshUserToken().
What a claim can hold#
| Rule | Limit |
|---|---|
| Values | JSON: text, numbers, true / false, null, lists and objects. |
| Nesting | Lists and objects inside each other, at most 5 levels deep. |
| Size | At most 1000 bytes for all claims of a user, written as JSON. |
| Names | Up to 64 characters. Names the token itself uses are refused, for example sub, iss, aud, iat, exp, nbf, jti, auth_time and amr (the error message names the one that failed). |
A refused call answers 400 INVALID_CLAIMS with the first problem, and changes nothing.
A token is signed, not encrypted: whoever holds the device can read it. Put roles, plans and ids in claims. Never e-mail addresses, phone numbers, prices or keys.
Good to know#
- On every plan. Claims cost nothing extra: a user with claims is still one monthly active user.
- Sessions stay. Changing claims never signs a user out. To cut a user off, use revokeRefreshTokens or disable the user in the Console.
- Anonymous users can have claims, and keep them when they upgrade to a full account.
- Open Sync listeners keep the old claims until they renew, at most 24 hours. For paid content that a user can lose, also check the claim in a normal read, or end the listener in the app when your server removes the claim.
- In the Console, Abrova Auth › Users marks users that have claims and shows them, read-only. Every change appears in Activity as "Custom claims changed", without the values.
- Tokens from your own server (
POST /users/tokenwithclaims) follow the same rules and put the claims in the same place, so rules read them the same way.
Last updated: 5 Nov 2026