Skip to content

Identify users

The widget loads for every visitor after init. Identify a signed-in user so votes, posts, and conversations attach to their account instead of an anonymous session.

Identification is verified-only. Your server signs a short-lived JWT and the browser sends that token as ssoToken. Quackback rejects unsigned { id, email, name } from the client.

The signing secret lives in your host app server environment (any name). It is not a Quackback Cloud or Quackback-host env var. Do not set QUACKBACK_WIDGET_SECRET on the Quackback instance.

const { ssoToken } = await fetch("/widget-token").then((r) => r.json());
Quackback.identify({ ssoToken });
// Script tag: Quackback("identify", { ssoToken })

Anonymous visitors keep using the widget after init. A session mints on their first write (uploads or reactions). Identification always uses a backend-signed ssoToken.


Identify signed-in users

Call identify after login, on page load if the session already exists, and whenever the signed-in user changes. Once per session, not on every navigation.

  1. Get the host signing secret (agent prompt on Install, or Reveal / Copy). Any server-only env name works.
  2. Sign a JWT (HS256, ~5 minutes) with sub and email
  3. Send { ssoToken } to the page the same way you already expose session data
  4. Call Quackback.identify({ ssoToken })

Never put the signing secret in client-side code, commits, or logs. Sign tokens on your server only. Any server-only env name works.


JWT claims

ClaimRequiredNotes
subYesStable unique ID from your system. id is accepted as an alias. Do not use email as sub.
emailYesUsed for notifications and matching existing accounts
nameNoDisplay name in the dashboard
avatarUrl / avatarURLNoAvatar image URL
segmentsNoArray of segment slugs. Unknown slugs are skipped. Dropping a slug removes that widget-sourced membership.
expRecommendedUnix timestamp. Use about 5 minutes from now.
customNoExtra keys become user attributes if you configured them

sub is the durable cross-device key. If the host app changes the user's email, the same sub still resolves to the same Quackback account.

Reserved JWT fields are not stored as attributes: sub, id, email, name, avatarURL, avatarUrl, segments, iat, exp, nbf, iss, aud, jti.

Team and admin accounts cannot be identified through the widget. A token whose sub or email matches a staff user returns IDENTITY_NOT_ALLOWED.

Identify signs the visitor in as a customer in the widget only. That sign-in never unlocks the admin dashboard, even if you later invite the same person to your team. See Session types.


Sign a token on your server

These names live in your app. Paste the value from Admin → Settings → Widget → Install. Quackback Cloud and self-host do not define this variable.

// app/api/widget-token/route.ts
import { SignJWT } from "jose";
import { getServerSession } from "next-auth";
 
const secret = new TextEncoder().encode(process.env.WIDGET_SIGNING_SECRET);
 
export async function GET() {
  const session = await getServerSession();
  if (!session?.user) {
    return new Response("Unauthorized", { status: 401 });
  }
 
  const token = await new SignJWT({
    email: session.user.email,
    name: session.user.name,
  })
    .setProtectedHeader({ alg: "HS256" })
    .setSubject(session.user.id)
    .setExpirationTime("5m")
    .sign(secret);
 
  return Response.json({ ssoToken: token });
}

Pass the token to the widget

const { ssoToken } = await fetch("/widget-token").then((r) => r.json());
Quackback.identify({ ssoToken });

Mint a fresh token on each login or page load. Short expiry limits the blast radius if one is intercepted.


Bundle identity into init

If you already have a token when the widget loads, pass it on init and skip a separate identify call:

Quackback.init({
  instanceUrl: "https://feedback.example.com",
  identity: { ssoToken },
});

Omit identity for anonymous visitors, or if you will call identify after async auth resolves.


Anonymous visitors

Do nothing. After init, the launcher is visible and the visitor is anonymous.

Quackback.init({ instanceUrl: "https://feedback.example.com" });

A session is not created up front. It mints on the first write (uploads, reactions, and similar). There is no { anonymous: true } flag to pass.

If they later sign in and you call identify with ssoToken, anonymous activity merges onto their account.


Switch users and log out

Call identify again with a new token when the signed-in user changes. You do not need logout in between.

Quackback.identify({ ssoToken: userBToken });

On logout, clear identity. The widget stays visible in anonymous mode. If the panel is open, it closes.

Quackback.logout();

Custom attributes

Pass extra claims such as plan, company, or mrr in the signed JWT. Matching attributes show on the user's profile.

// Server-side payload
const payload = {
  sub: user.id,
  email: user.email,
  name: user.name,
  plan: "pro",
  company: "Acme Corp",
  mrr: 299,
};

Configure attributes in Admin → Settings → People → Attributes first. Unknown keys are ignored, not rejected.


Sign portal users in from the widget

If you run a private portal, a verified widget visitor can open the portal already signed in, with no password, no second sign-up.

  1. Identify widget users with ssoToken (this page)
  2. Turn on Widget sign-in under Admin → Settings → Security → Authentication → Portal access

They stay a customer on the portal. Opening the portal from the widget does not grant admin access. See Control portal access and Session types.


Rotate the secret

If the signing secret is exposed, regenerate it from Admin → Settings → Widget → Install, then deploy the new value in your product. The old secret stops working immediately. The launcher is unaffected.

Regenerate and deploy together. Identify calls fail until your server signs tokens with the new secret.


Next steps