Abrova
☰ Docs menu · Custom claims

Custom claims

View as Markdown

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.

One fact every product can trust.

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#

  1. Your server sets the claims for a user, with the project's server secret: setCustomUserClaims(userId, { plan: 'pro' }).
  2. Every new token of that user carries them, at the top level, next to sub and exp. 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.
  3. Sync security rules read them as auth.token.plan, and the app reads them with getTokenResult() 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:

ts
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:

bash
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.

The server secret stays on your server.

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:

ts
const user = await auth.getUser(userId);
await auth.setCustomUserClaims(userId, { ...user.customClaims, beta: true });

To remove all claims, send null (or {}):

ts
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:

json
{
  "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:

ts
// Web, React Native
const result = await auth.getTokenResult();
if (result?.claims.plan === 'pro') showProBadge();
dart
// Flutter
final result = await auth.getTokenResult();
if (result?.claims['plan'] == 'pro') showProBadge();
swift
// iOS
let result = try await AbrovaAuth.shared.getTokenResult()
if result?.claims["plan"] as? String == "pro" { showProBadge() }
kotlin
// Android
val result = AbrovaAuth.shared.getTokenResult()
if (result?.claims?.get("plan") == "pro") showProBadge()
FieldWhat it is
tokenThe access token itself, the same one getToken() gives.
claimsEvery field of the token: sub (the user id), pid, iat, exp, auth_time, amr, and your custom claims.
expirationTimeWhen this token expires (iOS: expirationDate, Android: expirationTimestamp).
issuedAtTimeWhen this token was issued (iOS: issuedAtDate, Android: issuedAtTimestamp).
authTimeWhen the user signed in. A renewal does not change it (iOS: authDate, Android: authTimestamp).
signInProviderHow the user signed in: anonymous, apple, google, password, magic_link or telegram.
For showing, not for deciding.

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:

ts
await auth.getTokenResult({ forceRefresh: true });

Then tell Abrova Sync to use it: sync.refreshUserToken().

What a claim can hold#

RuleLimit
ValuesJSON: text, numbers, true / false, null, lists and objects.
NestingLists and objects inside each other, at most 5 levels deep.
SizeAt most 1000 bytes for all claims of a user, written as JSON.
NamesUp 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.

Not for personal or secret data.

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/token with claims) follow the same rules and put the claims in the same place, so rules read them the same way.
Was this page helpful?
Report a problem

Last updated: 5 Nov 2026