> ## Documentation Index
> Fetch the complete documentation index at: https://docs.extra-ai.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Identity

> Give every user their own private chat — in most cases without writing any code.

Every conversation belongs to one person. Nobody else can list it, read it, or
write to it, even knowing its id.

Who that person is depends on your product:

<CardGroup cols={3}>
  <Card title="No login" icon="user">
    No accounts, or a public page. **Nothing to do.**
  </Card>

  <Card title="Same domain" icon="shield-check">
    Extra served from your site. **Config only.**
  </Card>

  <Card title="Other domain" icon="key">
    Extra on its own hostname. **One endpoint.**
  </Card>
</CardGroup>

***

## No login

```html theme={null}
<script type="module" src="https://agents.acme.com/widget.js"></script>
<agent-chat title="Support"></agent-chat>
```

Each browser gets its own signed pass and its own conversations. Two people on
the same page cannot see each other's chats.

***

## Same domain

Serve Extra from a path on the site your users already sign into — say
`acme.com/agents` — and it reads the session they already have.

```bash theme={null}
EXTRA_AUTH_MODE=host_token
EXTRA_AUTH_SECRET=${YOUR_JWT_SECRET}   # the key your app already signs with
EXTRA_AUTH_COOKIE=session              # your session cookie's name
```

```html theme={null}
<script type="module" src="/agents/widget.js"></script>
<agent-chat title="Support"></agent-chat>
```

No code. The browser sends your cookie because it's the same origin, and Extra
verifies it with your own secret.

<Note>
  Works with any app that issues a session JWT — Open WebUI, Django, Rails, most
  Node stacks. If your user id isn't in the `sub` claim, set
  `EXTRA_AUTH_CLAIM_USER_ID`.
</Note>

***

## Other domain

Browsers won't send your session cookie to a different hostname, so your backend
vouches for the user instead.

```js theme={null}
app.get("/agent-chat/token", requireLogin, (req, res) => {
  res.json({
    token: jwt.sign({ sub: req.user.id }, process.env.EXTRA_AUTH_SECRET, {
      expiresIn: "1h", // required: tokens minted for us must expire
    }),
  });
});
```

```bash theme={null}
EXTRA_AUTH_MODE=mint
EXTRA_AUTH_SECRET=<32+ random characters>
```

```html theme={null}
<agent-chat title="Support" token-url="/agent-chat/token"></agent-chat>
```

Not logged in? Return `401` — the widget falls back to an anonymous pass.

<Note>
  `token-url` is resolved like any other browser request: relative to the page.
  That is correct in production, where your app and its API share an origin. If
  your **dev** setup serves the frontend separately from your API, point it at the
  same base your own frontend uses.
</Note>

### When identity fails

Anything other than a clean token is reported — a wrong URL, an endpoint
answering with the wrong shape, an unreachable host. The widget logs a warning
and raises an event, so a broken integration cannot quietly look like a working
anonymous chat:

```js theme={null}
document.querySelector("agent-chat").addEventListener("agent-chat:identity-error", (e) => {
  // { reason: "unauthorized" | "unreachable" | "malformed", status, url,
  //   anonymousFallbackEnabled }   <- whether falling back is allowed, not that it worked
  console.log(e.detail);
});
```

By default it then continues as an anonymous visitor. If a chat with no proven
user is unacceptable in your product, opt out of that fallback entirely:

```html theme={null}
<agent-chat title="Support" token-url="/agent-chat/token" require-identity></agent-chat>
```

<Warning>
  Sign `req.user.id` from the session, never a value from the request. An endpoint
  that accepts `?user=` lets anyone get a token as anyone.
</Warning>

***

## Signing in keeps the conversation

Someone who chats before logging in doesn't lose it. On their first
authenticated request the widget hands their pass over and those conversations
move onto their account. Automatic, and it only happens once.

## Signing in and out

A normal page navigation needs nothing — the widget works out who the caller is
on every load. A single-page app that signs a user **in or out without
reloading** should say so, or the widget keeps the identity it already resolved:

```js theme={null}
const chat = document.querySelector("agent-chat");

chat.refreshIdentity();  // signed in, or switched user
chat.logout();           // signed out
```

<Warning>
  Use `refreshIdentity()` on sign-**in**, not `logout()`. `logout()` discards the
  visitor pass, and that pass is what carries a visitor's earlier conversations
  onto their account — calling it here throws them away instead of merging them.
</Warning>

## Settings

| Variable                      | Default     |                                                                     |
| ----------------------------- | ----------- | ------------------------------------------------------------------- |
| `EXTRA_AUTH_MODE`             | `anonymous` | `anonymous`, `host_token`, or `mint`                                |
| `EXTRA_AUTH_SECRET`           | —           | Signing key, 32+ characters. Required unless anonymous              |
| `EXTRA_AUTH_COOKIE`           | —           | Your session cookie's name (`host_token` only)                      |
| `EXTRA_AUTH_CLAIM_USER_ID`    | `sub`       | Claim holding the user id. Also `_EMAIL`, `_DISPLAY_NAME`, `_ROLES` |
| `EXTRA_AUTH_MAX_TTL_SECONDS`  | `3600`      | Longest token accepted in `mint` mode                               |
| `EXTRA_AUTH_ANONYMOUS_SECRET` | derived     | Signs anonymous passes. Set it to survive a secret rotation         |

Rotating `EXTRA_AUTH_SECRET` is safe: widgets fetch a fresh token on their next
request.
