Signed sign-ins
In a live environment a sign-in counts only when your backend vouches for it. That is one line of HMAC on your server and one extra argument in your app.
Why a sign-in needs your server
Your write key is public — it ships inside your app and your web pages — so an app saying “this is account user_42” proves nothing on its own. Without a check, anyone holding the key could sign their own phone in as one of your users: receive that person's push notifications, read their in-app messages and inbox, and write their email address into the profile your campaigns send to.
Only your backend knows who really signed in. So it signs the account id with a secret the app never sees, and Treebars records the sign-in only when the signature matches. A test environment does not ask unless you turn it on, so you can start building with no backend at all — and turn it on to rehearse production before you ship.
How it fits together
1Put the identity secret on your server
In the dashboard, Settings → Environments, an admin reveals the environment's identity secret — it starts with tbis_ — and puts it in your backend's configuration, the way you keep any other API secret. Each environment has its own. It never goes into the app.
2Sign the account id when somebody signs in
After your backend has checked the person's password (or token, or SSO), it computes the signature and sends it back with the rest of the sign-in response.
3Pass the signature to identify
The app hands it to the SDK with the account id. The SDK sends it with the sign-in and with every read of that person's messages, and keeps it until reset().
Your backend
The signature is the hex HMAC-SHA256 of the account id, keyed with the whole secret, prefix included. Sign exactly the string your app passes to identify — the same characters, the same case.
import { createHmac } from 'node:crypto';
const signature = createHmac('sha256', process.env.TREEBARS_IDENTITY_SECRET!)
.update(user.id)
.digest('hex');import hashlib, hmac, os
signature = hmac.new(
os.environ["TREEBARS_IDENTITY_SECRET"].encode(), user.id.encode(), hashlib.sha256
).hexdigest()mac := hmac.New(sha256.New, []byte(os.Getenv("TREEBARS_IDENTITY_SECRET")))
mac.Write([]byte(user.ID))
signature := hex.EncodeToString(mac.Sum(nil))signature = OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("TREEBARS_IDENTITY_SECRET"), user.id)$signature = hash_hmac('sha256', $user->id, getenv('TREEBARS_IDENTITY_SECRET'));In context, the sign-in route returns the signature beside the session it already returns:
app.post('/api/sign-in', async (req, res) => {
const user = await checkPassword(req.body.email, req.body.password);
if (!user) return res.status(401).json({ error: 'wrong_credentials' });
// Only now, for the person you just authenticated.
const signature = createHmac('sha256', process.env.TREEBARS_IDENTITY_SECRET!)
.update(user.id)
.digest('hex');
res.json({ user, session: await openSession(user), signature });
});Your app
Pass the signature with the account id, wherever you already identify the person:
// Web
treebars.identify(user.id, { email: user.email }, signature);
// React Native
await treebars.identify(user.id, { email: user.email }, signature);
// iOS
Treebars.identify(user.id, attributes: ["email": user.email], signature: signature)
// Android
Treebars.identify(user.id, mapOf("email" to user.email), signature)When to sign
Once per sign-in, not per event. Events need no signature: the SDK attaches the person to them by itself. The signature does not expire; it stays valid until you rotate the secret. The simplest pattern is to return it wherever your backend already confirms a session — at sign-in, and on the “who am I” call your app makes when it launches — and call identify with it each time. That also carries a rotated secret to every device by itself.
Rotating the secret
Settings → Environments → Rotate makes a new secret. The old one keeps working for a day, so you can deploy the new one to your backend without anybody's sign-in or inbox failing in between. Signatures made with the old secret stop verifying after that day, so apps that re-identify on launch — as above — pick up the new one on their own.
Turning it on and off
Every environment has a switch under Settings → Environments. It is on in a live environment and off in a test one until an admin moves it.
Turn it on in your test environment before you ship. Test then refuses an unsigned sign-in exactly as production does, so an app that signs people in on test will not leave them anonymous in production. That is the check that your backend signs, your app passes the signature, and both use the right secret.
An admin can turn it off in a live environment, for an app with no backend to sign with yet. Understand what that trades before you do: with it off, anyone holding your write key can sign a device in as any of your users and read that person's messages. Turn it back on as soon as your backend signs.
When somebody signs in and stays anonymous
The SDK logs it once: This environment requires a signed identity…. The sign-in was refused, and nothing about the person was recorded. Check, in order:
What to check
- The app called identify without the third argument, or your backend returned null.
- Each environment has its own. Production’s secret does not sign for Test, or the other way round.
- The signature has to be of exactly the account id passed to identify — "User_42" and "user_42" are different ids.
- Send the hex digest: 64 characters, 0–9 and a–f.
- The old secret has stopped verifying. Put the new one in your backend.