How to tell an embedded AI assistant who is signed in
The short answer
Have your own backend mint a short-lived token naming the user, signed with a key the browser never sees, and give the widget a function that fetches a fresh one rather than a single token. A single token works perfectly until it expires, then the assistant starts telling a signed-in user to sign in. Return null only when nobody is signed in, never when the request failed, and encode the token as base64url without padding.
An AI assistant embedded with a script tag runs on your page but is served from somewhere else. That is what makes it easy to install, and it is also why it cannot see who is signed in. Your session cookie belongs to your origin, and a well-behaved browser will not hand it to anyone else.
So the assistant starts every visitor as anonymous, including somebody who has been signed into your app all morning. For an assistant that only answers questions that is fine. For one that takes actions it is the difference between working and refusing every write.
Let your backend say who it is
Your server already knows who is signed in. It states that in a short-lived token signed with a key only your server and the assistant’s server hold, and the assistant verifies the signature rather than trusting anything the page claims. Nothing exotic: an HMAC over a small JSON body, which every backend language does in its standard library.
const claims = { sub: user.id, exp: Math.floor(Date.now() / 1000) + 900 };
const body = base64url(JSON.stringify(claims));
const sig = hmacSha256(body, SIGNING_KEY); // raw digest, then base64url
return { token: `${body}.${sig}` };Two details decide whether that works. exp is in seconds, not milliseconds. And both halves are base64url with no padding: - and _ instead of + and /, and no trailing =. Standard base64 produces a token that looks completely correct in a log and fails every signature check.
Give it a function, not a token
This is the mistake that does not look like one for fifteen minutes.
The obvious integration fetches a token at page load and hands it over once. It works. The demo works. Then the token expires, because it is short-lived on purpose, it is a bearer credential for a person, and the assistant has no way to get another. A signed-in user starts being told to sign in.
Hand over a function that fetches a fresh token instead, and let the assistant call it before the current one runs out and once more after an unexpected rejection:
const getSessionToken = () =>
fetch("/api/assistant-token", { credentials: "same-origin", cache: "no-store" })
.then((r) => r.json())
.then(({ token }) => token);Null means signed out, and only that
That fetch can come back empty for three different reasons: nobody is signed in, the endpoint returned an error, or the request never completed. It is tempting to return null for all three.
Do not. Signed out is a real state with a real consequence: the assistant clears the conversation, so the next person at a shared machine does not see it. If a blipped request also returns null, one dropped connection wipes a real user’s conversation halfway through a task. Keep three outcomes apart: signed in, signed out, and unknown.
Two checks before calling it done
- The signing key stays on the server. It never goes in code that reaches the browser, where anybody could mint a token for any user.
- A cross-origin API needs the full URL. If your API lives on another host, a relative path quietly fetches your own app’s HTML, JSON parsing fails inside the promise, and the assistant stays anonymous with nothing in the console pointing at identity.
What the token protects is covered in how to let users take actions with AI, and what happens when the identity changes is in the chat widget that shows the last user’s conversation.
Common questions
Why can't the assistant just use my session cookie?
A widget loaded from another origin cannot read your cookies, and it should not be able to. Your backend already knows who is signed in, so it states that in a signed token, and the assistant's server verifies the signature instead of trusting the page.
What goes in the token?
As little as possible: the user's id in your system and an expiry a few minutes out, in seconds rather than milliseconds. It is a bearer credential for a person, so keep it short-lived and let the widget fetch a new one before it runs out.
What should happen for a visitor who is not signed in?
The assistant should carry on anonymously and read-only, enforced on the server. That is a working state, not an error, which is exactly why a failed token request must not be reported as signed out: it would wipe a real user's conversation.
Keep reading
- Your AI chat widget is showing the last user's conversation
A real shared-device leak: permissions held, the transcript did not. Keying on the subject, not the token, and why sign-out has no single choke point.
- Postgres row-level security for a multi-tenant AI agent
The isolation boundary, the tables that deliberately sit outside it, and why the loud failure is the one worth engineering for.
- Your coding agent can set up your product’s AI agent now
The setup that used to mean a dashboard, three tabs and a key to paste is now one sentence to the coding agent you already have open.
Verb is this, built. An AI assistant you embed in your SaaS with one script tag: it calls your own API as the signed-in user, confirms before it changes anything, and logs every action. Free to build and test.