Skip to content
Private betaTreebars is letting teams in from the waitlist.Join the waitlist

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.

Nodets
import { createHmac } from 'node:crypto';

const signature = createHmac('sha256', process.env.TREEBARS_IDENTITY_SECRET!)
  .update(user.id)
  .digest('hex');
Pythonpython
import hashlib, hmac, os

signature = hmac.new(
    os.environ["TREEBARS_IDENTITY_SECRET"].encode(), user.id.encode(), hashlib.sha256
).hexdigest()
Gogo
mac := hmac.New(sha256.New, []byte(os.Getenv("TREEBARS_IDENTITY_SECRET")))
mac.Write([]byte(user.ID))
signature := hex.EncodeToString(mac.Sum(nil))
Rubyruby
signature = OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("TREEBARS_IDENTITY_SECRET"), user.id)
PHPphp
$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:

Your sign-in routets
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 });
});
Never sign an account id you have not just authenticated. A route that signs whatever id it is sent gives anyone the same reach as your secret — the protection is only as good as your sign-in.

Your app

Pass the signature with the account id, wherever you already identify the person:

The apptsx
// 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.

The signature is not a secret. It proves one thing — that your backend vouched for this one account id — so it is safe on the device and in transit. The identity secret is the secret, and it never leaves your server.

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

No signature at all
The app called identify without the third argument, or your backend returned null.
Another environment’s secret
Each environment has its own. Production’s secret does not sign for Test, or the other way round.
A different string
The signature has to be of exactly the account id passed to identify — "User_42" and "user_42" are different ids.
Base64 instead of hex
Send the hex digest: 64 characters, 0–9 and a–f.
A secret rotated over a day ago
The old secret has stopped verifying. Put the new one in your backend.