Authentication
Prove who your users are with short-lived user tokens minted by your server — Flutter, iOS, Android, React Native, Web, Node.js, REST. Automatic refresh, revocation on logout, and strict mode.
Your API key identifies your project, not your user. It ships inside every app and web bundle, so anyone can extract it. To let AbrovaChat trust who is acting, your server issues a short-lived user token and the SDK sends it with every request.
This is the same model used by other chat platforms: your backend vouches for the user, the client never holds a secret.
User tokens are opt-in. Without a tokenProvider the SDK uses the legacy mode (API key + userId), exactly as before. Add tokens when you are ready, then switch your project to strict mode.
The three credentials#
| Credential | Lives in | Proves | If leaked |
|---|---|---|---|
API key (ab_live_…) | Your app, public | Which project | Harmless in strict mode: it cannot act as a user |
Server secret (ab_srv_…) | Your server only | That your backend is calling | Serious — rotate it in Abrova Console |
| User token (JWT, 1 h) | Your app, per user | Which user | Limited: one user, one project, expires, and you can revoke it |
Mint tokens on your server#
Add one endpoint, protected by your own login, that returns a token for the signed-in user.
Node.js
import { AbrovaChat } from '@abrova/chat';
const chat = new AbrovaChat({
apiKey: process.env.ABROVA_API_KEY!,
serverSecret: process.env.ABROVA_SERVER_SECRET!, // server-side only
});
// Express — requireLogin is YOUR auth middleware
app.get('/chat-token', requireLogin, async (req, res) => {
const { token, expiresAt } = await chat.users.createToken({
userId: req.user.id, // your own user id
info: { displayName: req.user.name }, // optional, shown to other clients
});
res.json({ token, expiresAt });
});REST (any backend)
curl -X POST https://chat.abrova.ir/api/v1/users/token \
-H "x-api-key: $ABROVA_API_KEY" \
-H "x-server-secret: $ABROVA_SERVER_SECRET" \
-H "Content-Type: application/json" \
-d '{"userId": "user-123", "ttl": 3600}'
# → { "token": "eyJhbGciOi…", "expiresAt": "2026-09-12T10:00:00.000Z", "userId": "user-123" }ttl is in seconds: 3600 (1 hour) by default, 86400 (24 hours) maximum. Shorter is safer, and refreshing costs the client one call to your server.
The server secret can mint a token for any user. Keep it on your server, out of app bundles, mobile binaries and client-side JavaScript.
One token for every Abrova product
If your app also uses Abrova Sync, Abrova Push or Abrova A/B Testing, mint one token for all of them from the Abrova auth endpoint:
curl -X POST https://auth.abrova.ir/users/token \
-H "x-api-key: $ABROVA_API_KEY" \
-H "x-server-secret: $ABROVA_SERVER_SECRET" \
-H "Content-Type: application/json" \
-d '{"userId": "user-123", "expiresIn": 3600}'
# → { "token": "eyJhbGciOi…", "userId": "user-123", "expiresIn": 3600, "expiresAt": "2026-09-21T10:00:00.000Z" }Give that same token to every SDK's tokenProvider (or setUserToken) — Chat, Sync, Push and A/B Testing — instead of fetching one per product. expiresIn is in seconds: 3600 (1 hour) by default, 86400 (24 hours) maximum. The same rules apply: call it from your server, never from an app.
A token is valid until it expires, so store it with the user's session (Keychain, Keystore or secure storage) and reuse it across app launches. Your app then only calls your backend when the token is close to expiry — not on every start.
Revoking a user (below) also blocks their Abrova token in Chat. It carries no info, so if other clients should see a display name from the token, mint the Chat token above instead.
Use the token in your app#
Pass a tokenProvider that calls your endpoint. The SDK caches the token, refreshes it about a minute before it expires, and retries once if the server reports an expired token — all invisible to the user.
Flutter
// abrova_chat: ^0.1.5 final client = AbrovaChatClient(AbrovaChatConfig( apiKey: 'ab_live_your_api_key', userId: currentUser.id, tokenProvider: () => myBackend.chatToken(), // Future<String> )); // Identity errors a refresh cannot fix (revoked, invalid, logged out). client.onAuthError.listen((e) => signOut());
Web
// @abrova/web-chat: ^0.1.2
const client = new AbrovaChatClient({
apiKey: process.env.NEXT_PUBLIC_ABROVA_API_KEY!,
userId: currentUser.id,
tokenProvider: async () => (await fetch('/chat-token')).json().then((j) => j.token),
});
client.on('authError', () => signOut());React Native
// @abrova/react-native-chat: ^0.1.2
const client = new AbrovaChatClient({
apiKey: ABROVA_API_KEY,
userId: currentUser.id,
tokenProvider: async () => {
const res = await fetch('https://api.example.com/chat-token', {
headers: { Authorization: `Bearer ${sessionToken}` },
});
return (await res.json()).token;
},
});
client.on('authError', () => signOut());iOS
// Swift Package Manager: https://github.com/Abrova-co/abrova-chat-ios-sdk.git (0.1.4+)
let config = AbrovaChatConfig(
apiKey: "ab_live_your_api_key",
userId: currentUser.id,
tokenProvider: { try await myBackend.chatToken() }
)
let client = AbrovaChatClient(config: config)
client.onAuthError
.sink { _ in signOut() }
.store(in: &cancellables)Android
// implementation("ir.abrova:abrova-chat-android:0.1.4")
val config = AbrovaChatConfig(
apiKey = "ab_live_your_api_key",
userId = currentUser.id,
tokenProvider = { myBackend.chatToken() } // suspend fun returning the token
)
val client = AbrovaChatClient(config)
scope.launch { client.onAuthError.collect { signOut() } }REST (any HTTP client)
curl https://chat.abrova.ir/api/v1/rooms \ -H "x-api-key: $ABROVA_API_KEY" \ -H "x-user-token: $USER_TOKEN"
Revoke on logout#
Tokens expire on their own, but revoke them when a session ends so a stolen token dies immediately.
await chat.users.revokeTokens(userId);
// REST: POST /api/v1/users/{userId}/revoke-tokens (server secret required)Every token issued to that user before this moment stops working, for REST and realtime alike, within a few seconds. Call it on logout, password change and ban.
What the server enforces#
Once a request carries a user token, AbrovaChat ignores any userId in the request and uses the token's user:
| Attempt | Result |
|---|---|
| Read or send as another user | 403 user_mismatch |
| Open a room you are not in (details, history, pinned, presence, reactions) | 403 not_a_participant |
| Edit or delete someone else's message | 403 |
| Use a token from another project | 401 token_invalid |
| Use an expired token | 401 token_expired — the SDK refreshes and retries automatically |
| Use a revoked token | 401 token_revoked |
Error codes the SDKs act on:
| Code | Meaning | SDK behaviour |
|---|---|---|
token_expired | Lifetime elapsed | Fetches a new token and retries the request once |
token_invalid | Malformed, wrong project, or not server-minted | authError — send the user to login |
token_revoked | Revoked by your server | authError |
token_required | Project is in strict mode and no token was sent | authError |
token_provider_failed | Your endpoint failed or is unreachable | authError |
Turn on strict mode#
While a project allows the legacy mode, a request with only the API key is still accepted — which is what keeps already-released apps working. Once every live client sends tokens, enable Require signed user tokens under Chat security in your project's settings in Abrova Console.
From then on, for that project:
- every per-user request needs a user token (or your server secret);
- the legacy connection-token endpoint (deprecated, used only by old SDK versions) no longer issues a token to the public API key alone.
Apps already installed keep using the legacy mode. Enable strict mode only once those versions are gone — a force-update mechanism in your app makes that possible. Server-to-server calls with the server secret are unaffected.
Room management from your server#
Creating rooms and adding or removing participants take no acting user, so with the API key alone anyone could add themselves to a room. Do that work on your server with the server secret, then enable Server-only room management under Chat security in your project's settings in Abrova Console.
// Your server, with the server secret
const { room } = await chat.rooms.findOrCreate({
externalId: `dm-${a}-${b}`,
participants: [{ externalUserId: a }, { externalUserId: b }],
});Apps keep sending messages, reading history and subscribing as usual; only room management is restricted.