Yak Docs
Customization

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

  1. Your backend signs the user id with your application's API secret using HMAC-SHA256.
  2. The signed identity is passed to <YakProvider> in the browser.
  3. The widget forwards { id, hash } to Yak on every request that touches that user's data.
  4. 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 new user prop (or omit it / pass undefined on logout) and the provider re-threads the identity for you.
  • The Vue / Svelte / Angular / Nuxt SDKs return a handle from createYakProvider; call setUser() from your own reactivity (a Vue watch, Svelte $effect, or Angular effect) whenever the signed-in user changes. Pass undefined to 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

FieldTypeRequiredDescription
idstringYesStable user id from your system. HMAC-protected.
hashstringYesHex 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:

AnonymousIdentified (user passed)
Conversations storedYes, when storage is onYes, when storage is on
Keyed toThe browser sessionYour user id
Survives a session resetNoYes
Follows the user to another deviceNoYes

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/chat when user is 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/transcript doesn't take a user. 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.

On this page