Assistant UI
Identity
The publishable key decides which agent replies. Identity tells Teaflask who is asking. Without it, every visitor is anonymous. That's fine for a marketing site, but on a page behind your login you'll want the assistant to know which of your users it's talking to.
Identity is a claim, never a capability. It says who the user is. It doesn't let the assistant act as them, and it doesn't widen what the agent may do. Actions run through your actions adapter, in the user's own session, and your API decides what they're allowed.
What identifying a user changes
| Anonymous visitor | Identified user | |
|---|---|---|
| Conversation belongs to | This browser | The user |
| Resumes | On reload, in the same browser | On any device they sign in on |
| History menu | None | Their past conversations, up to 200 |
| Per-user limits | Don't apply | Apply |
| Agents set to require sign-in | Can't chat | Can chat |
A user who starts a conversation anonymously and then signs in keeps it. When the first identified session starts in that browser, the conversation becomes theirs.
How it works
Your backend vouches for the user, and the assistant carries that vouch to Teaflask:
- Your backend signs a short-lived token, a JWT, that says "this is user 42". It signs with a secret only your backend and Teaflask know.
- Your page gives the assistant two things: the user's id,
userId, and a function that fetches a fresh token,getEndUserToken. - Whenever the assistant needs a new visitor token, at startup and before the current one expires, it calls
getEndUserTokenand sends the result to Teaflask. - Teaflask checks the signature, and checks that the token's user is the same as
userId. If both hold, the session is identified as that user.
The signing secret never reaches the browser. Only tokens signed with it do, and each one expires quickly.
Sign tokens on your server
The token is an HS256 JWT signed with your organization's end-user JWT secret, which you'll find under Settings → Keys → Identity verification.
| Claim | Required | Meaning |
|---|---|---|
user_id |
Yes | Your stable id for the user, as a non-empty string |
exp |
Yes | When the token expires. An hour or less is plenty |
email |
No | The user's email, for display and support context |
iat |
Recommended | When the token was issued |
Teaflask ignores other claims and doesn't require aud or iss. In Node:
import jwt from "jsonwebtoken";
export function teaflaskTokenFor(user: { id: string; email: string }) {
return jwt.sign(
{ user_id: user.id, email: user.email },
process.env.TEAFLASK_END_USER_JWT_SECRET!,
{ algorithm: "HS256", expiresIn: "1h" },
);
}
In Python:
import time
import jwt
def teaflask_token_for(user):
return jwt.encode(
{"user_id": str(user.id), "email": user.email, "exp": int(time.time()) + 3600},
TEAFLASK_END_USER_JWT_SECRET,
algorithm="HS256",
)
Serve the token from an endpoint that only a signed-in user can call, such as /api/teaflask-token. Take the user from your server's session, never from anything the browser sends. Return the token as plain text with Cache-Control: no-store.
Never put the signing secret in browser code, a public environment variable or a response. Anyone who has it can sign in as any of your users.
Give the assistant the pair
userId and getEndUserToken always go together. userId must be the same id your backend signs into user_id, and it should come from your app's own sign-in state, not from the token. getEndUserToken returns a promise of the token string. It's called each time a token is needed, so fetch a fresh one every time rather than caching the first.
import { TeaflaskAssistant } from "@teaflask/assistant";
async function getEndUserToken(): Promise<string> {
const response = await fetch("/api/teaflask-token", { cache: "no-store" });
if (!response.ok) {
throw new Error("Could not get a Teaflask token for this user.");
}
return response.text();
}
export function Assistant({ user }: { user: { id: string } | null }) {
return user ? (
<TeaflaskAssistant
publishableKey="pk_live_…"
userId={user.id}
getEndUserToken={getEndUserToken}/>
) : (
<TeaflaskAssistant publishableKey="pk_live_…"/>
);
}
On the script tag, set the same two properties on the element. Set them again whenever the user changes:
const assistant = document.querySelector("teaflask-assistant");
assistant.userId = currentUser.id;
assistant.getEndUserToken = getEndUserToken;
Under a strict Content Security Policy, the setup script is a separate file, so render the user's id into the page, for example as a data- attribute, and read it there. See Install.
What happens when something is off
| Situation | What the assistant does |
|---|---|
| Neither half is set | The visitor is anonymous |
Only one half is set, or userId is empty |
Runs anonymously, with one console warning |
getEndUserToken returns null |
Runs anonymously. Use this for a signed-out state |
| The token is expired, badly signed or malformed | Shows a setup error (END_USER_TOKEN_INVALID). It never quietly falls back to anonymous, so a broken signer shows up while you're building |
The token's user_id doesn't match userId |
Shows a setup error (END_USER_ID_MISMATCH) after one retry |
Sign-in, sign-out and switching users
The conversation belongs to userId. When it changes, including to nothing on sign-out, the assistant drops everything from the previous user at once: the open conversation, the history list, the visitor token and any pending prompts. Then it starts a fresh session. So keep the pair up to date wherever the assistant is mounted, and the right thing happens:
- On sign-in, set both halves. The anonymous conversation in this browser becomes the user's.
- On sign-out, remove both. In React, render without them. On the element, set both to
null. - On an account switch, set the new user's pair.
On the npm channel, also call resetAssistant in your sign-out handler. It makes the browser forget which conversation to resume, so the next person on a shared computer starts clean. It only touches browser storage, so it works on pages where the assistant isn't mounted:
import { resetAssistant } from "@teaflask/assistant";
resetAssistant({ publishableKey: "pk_live_…" });
Signing out doesn't delete anything on the server. A user's conversations stay theirs, and only they can open them again.
Require sign-in
An agent can be set to serve signed-in users only, under its Model & access settings. Then an anonymous visitor's messages are refused with ASSISTANT_SIGN_IN_REQUIRED, and the assistant asks them to sign in to your site. If you turn this on, wire identity on every page that carries the assistant.
Rotate the signing secret
Rotating the end-user JWT secret in the dashboard makes it reject tokens signed with the old secret immediately. There's no grace period. Your backend has to start signing with the new secret straight away, so have that change ready before you rotate.
Visitor tokens already issued are signed by Teaflask, not with your secret, so they keep working until they expire, within 15 minutes. Rotation isn't an instant sign-out of every user.
More resources
- Install – passing the identity pair on the npm package and the script tag
- Publishable keys – the other credential, which answers which agent replies, not who is asking
- Actions adapter – running actions in the user's own session, a separate question from who they are
- Surfaces – why every surface needs the same identity wiring to share a conversation
- Transcript – history and resuming for identified users