End-user identity
Pass a signed user identity into the widget and Yak will attribute conversations to that user. When the same user returns — on a new device, or in a new browser — the history pane shows the chats they've had with you before, so they can resume one or start fresh.
Identity controls who a conversation belongs to, not whether it is stored. Storage is a separate per-application setting — see Conversation storage, which is off by default on new applications. Without storage on, passing a user changes nothing that persists.
This is opt-in. If you don't pass a user, conversations are still stored when storage is on, but they're keyed to the visitor's session rather than an account: history follows the browser session and is lost when it resets, and it can't follow the person to another device.
How it works
- Your backend signs the user id with your application's API secret using
HMAC-SHA256. - The signed identity is passed to
<YakProvider>in the browser. - The widget forwards
{ id, hash }to Yak on every request that touches that user's data. - Yak verifies the hash against the secret and stamps the conversation with that user id, so it can be listed back to them later.
There is no separate "end-user" record to manage. An end-user exists exactly as far as their conversations do — verification is a pure HMAC check against your secret, with nothing stored about the person themselves.
The secret never leaves your server. The hash proves the browser is acting on behalf of a real account from your system — without it, anyone with an end-user's id could impersonate them.
This is the same identity-verification model used by Intercom and similar widget vendors. Only the user id is signed, so the identity is tamper-proof.
Set it up
Grab your API secret
Open your application's Customer settings and reveal the End-user identity card. Copy the secret somewhere safe — treat it like any other backend credential.
Store it as an environment variable on your server (never expose it to the browser):
YAK_API_SECRET=...Sign the user id on your server
Compute an HMAC-SHA256 hex digest of the user id using the secret:
import crypto from "node:crypto";
const userHash = crypto
.createHmac("sha256", process.env.YAK_API_SECRET!)
.update(currentUser.id)
.digest("hex");import hmac, hashlib, os
user_hash = hmac.new(
os.environ["YAK_API_SECRET"].encode(),
current_user.id.encode(),
hashlib.sha256,
).hexdigest()require "openssl"
user_hash = OpenSSL::HMAC.hexdigest(
"SHA256",
ENV["YAK_API_SECRET"],
current_user.id,
)import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"os"
)
mac := hmac.New(sha256.New, []byte(os.Getenv("YAK_API_SECRET")))
mac.Write([]byte(currentUser.ID))
userHash := hex.EncodeToString(mac.Sum(nil))Send the hash to the browser however you already pass user data (server-rendered HTML, a session API, etc.).
Pass the identity into the widget
Supply { id, hash } when you set up the widget. React/Next take it as a prop; the other SDKs take it as an option to createYakProvider.
import { YakProvider } from "@yak-io/react"; // or "@yak-io/nextjs/client"
<YakProvider
appId={process.env.NEXT_PUBLIC_YAK_APP_ID!}
user={{
id: currentUser.id,
hash: currentUser.yakHash, // from your server
}}
>
{children}
</YakProvider>import { createYakProvider } from "@yak-io/vue";
const yak = createYakProvider({
appId: "your-app-id",
user: { id: currentUser.id, hash: currentUser.yakHash },
});import { createYakProvider } from "@yak-io/svelte";
const yak = createYakProvider({
appId: "your-app-id",
user: { id: currentUser.id, hash: currentUser.yakHash },
});import { createYakProvider } from "@yak-io/angular";
const yak = createYakProvider({
appId: "your-app-id",
user: { id: currentUser.id, hash: currentUser.yakHash },
});import { createYakProvider } from "@yak-io/nuxt";
const yak = createYakProvider({
appId: "your-app-id",
user: { id: currentUser.id, hash: currentUser.yakHash },
});That's it. Open the widget and you'll see a new history icon in the header — click it to browse past conversations or start a new one.
Updating the user (login / logout)
Your end-user may sign in or out without a full page reload. Keep the widget's identity in sync so conversations follow the right account.
- React / Next.js are declarative — render
<YakProvider>with the newuserprop (or omit it / passundefinedon logout) and the provider re-threads the identity for you. - The Vue / Svelte / Angular / Nuxt SDKs return a handle from
createYakProvider; callsetUser()from your own reactivity (a Vuewatch, Svelte$effect, or Angulareffect) whenever the signed-in user changes. Passundefinedto log out.
import { watch } from "vue";
watch(currentUser, (u) => {
yak.setUser(u ? { id: u.id, hash: u.yakHash } : undefined);
});$effect(() => {
yak.setUser($currentUser ? { id: $currentUser.id, hash: $currentUser.yakHash } : undefined);
});// `currentUser` is a signal; re-run whenever it changes
effect(() => {
const u = currentUser();
yak.setUser(u ? { id: u.id, hash: u.yakHash } : undefined);
});import { watch } from "vue";
watch(currentUser, (u) => {
yak.setUser(u ? { id: u.id, hash: u.yakHash } : undefined);
});Switching to a different id (or logging out) rotates the stored session token, so one user's session is never reused under another account. The first sign-in after anonymous browsing keeps the session, so the visitor's pre-login conversation is claimed by their new account.
The user prop
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Stable user id from your system. HMAC-protected. |
hash | string | Yes | Hex HMAC-SHA256 of id signed with your application's API secret. |
What the end-user sees
Whenever conversation storage is on:
- A menu button (the hamburger) sits in the chat header.
- Opening it reveals Conversation history, which lists past conversations, most recently started first.
- Selecting a conversation re-hydrates the chat with the persisted messages — the user can keep talking right where they left off.
- New conversation in the same menu starts a fresh one without losing the old ones.
- Each conversation in the history list has a trash icon for removing it (and its messages) permanently.
Passing user doesn't add this UI — it changes which conversations the list can contain.
Anonymous vs identified
The history pane appears whenever storage is on, signed in or not. What differs is the key the conversations hang off:
| Anonymous | Identified (user passed) | |
|---|---|---|
| Conversations stored | Yes, when storage is on | Yes, when storage is on |
| Keyed to | The browser session | Your user id |
| Survives a session reset | No | Yes |
| Follows the user to another device | No | Yes |
If you don't pass user, conversations are still written server-side when storage is on — they're just session-keyed and anonymous. "No user" does not mean "nothing stored". To store nothing at all, turn conversation storage off.
You can mix the two modes inside the same application — for example, anonymous chat on a public marketing page and signed-in chat in your dashboard.
Rotating the secret
If you suspect the secret has leaked, rotate it from the dashboard. Rotation invalidates every previously signed hash, so deployed integrations will fail until you re-deploy with the new value. Plan the rotation alongside a deploy of your server code.
To force every current visitor onto a fresh session without touching your identity signing (no redeploy needed), use Sign out all sessions instead — see Session revocation.
Security model
- What's signed: only the user
id. A holder of the secret can produce a valid identity for any user id, so keep the secret on the server. - Where verification happens: the endpoints that accept an identity re-verify the hash on every request and check the user's ownership of the conversation —
/api/chatwhenuseris present, the conversation history endpoints, and the voice session endpoint that mints a realtime token. There are no session cookies; the signed identity rides along on each request. - Voice transcripts:
/api/voice/transcriptdoesn't take auser. The identity is verified once, up front, when the voice session is minted, and the transcript is authorized by the voice session id — which only counts as a write key for a session your application actually minted. The user id is read from that stored session rather than re-supplied by the browser.
For the broader security context, see the Security page.