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](/adapters), 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:

1. 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.
2. Your page gives the assistant two things: the user's id, `userId`, and a function that fetches a fresh token, `getEndUserToken`.
3. Whenever the assistant needs a new visitor token, at startup and before the current one expires, it calls `getEndUserToken` and sends the result to Teaflask.
4. 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:

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

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

```tsx
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:

```js
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](/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:

```ts
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](/install) – passing the identity pair on the npm package and the script tag
- [Publishable keys](/publishable-keys) – the other credential, which answers which agent replies, not who is asking
- [Actions adapter](/adapters) – running actions in the user's own session, a separate question from who they are
- [Surfaces](/surfaces) – why every surface needs the same identity wiring to share a conversation
- [Transcript](/transcript) – history and resuming for identified users
