Identify users
Attribute widget activity to signed-in users with a server-signed ssoToken: JWT claims, server examples, anonymous visitors, custom attributes, portal sign-in, and secret rotation.
The widget loads for every visitor after init. Identify a signed-in user so their votes, posts, and conversations attach to their account instead of an anonymous session. This article covers signing tokens on your server, the JWT claims, anonymous visitors, custom attributes, portal sign-in, and rotating the secret.
How identification works
Identification is verified-only. Your server signs a short-lived JWT with your workspace's signing secret, and the browser sends that token to the widget as ssoToken. Quackback rejects unsigned { id, email, name } sent from the browser.
The signing secret lives in your app's server environment, under any name you like. It is not a Quackback setting or a Quackback environment variable, and it is available on every plan. Do not set QUACKBACK_WIDGET_SECRET on your Quackback server.
const { ssoToken } = await fetch("/widget-token").then((r) => r.json());
Quackback.identify({ ssoToken });
// Script tag: Quackback("identify", { ssoToken })Note: Anonymous visitors can use the widget right after
init. Their session is created on their first action that needs one (for example an upload or a reaction).
Identify signed-in users
Call identify after login, on page load if the user is already signed in, and whenever the signed-in user changes. Once per session is enough, not on every navigation.
- Get the signing secret. The agent install prompt stores it for you. To copy it by hand, go to Settings > Widget > Install, find Signing secret, and use Reveal and Copy. Put it in a server-only environment variable in your app.
- Sign a JWT on your server (HS256, expiring in about 5 minutes) with
subandemail. - Send
{ ssoToken }to the page the same way you already expose session data. - Call
Quackback.identify({ ssoToken }).
Warning: Never put the signing secret in client-side code, commits, or logs. Sign tokens on your server only.
JWT claims
Claim | Required | Notes |
|---|---|---|
| Yes | Stable unique ID from your system. |
| Yes | Used for notifications and to match existing accounts |
| No | Display name |
| No | Avatar image URL |
| No | Array of segment slugs. Unknown slugs are skipped. Dropping a slug removes that widget-added membership. |
| Recommended | Unix timestamp, about 5 minutes from now |
custom | No | Extra keys become custom attributes if you configured them |
sub is the durable key across devices. If a user changes their email in your app, the same sub still resolves to the same Quackback account, and the account adopts the new email.
These claims are reserved and never stored as attributes: sub, id, email, name, avatarURL, avatarUrl, segments, iat, exp, nbf, iss, aud, jti.
Teammates in the widget
Identify signs the visitor in as a customer in the widget only. It never unlocks the admin dashboard, even when the email belongs to a workspace admin or member. Teammates can identify and use the widget the way a customer would, and their dashboard name and avatar are not overwritten by your app's token.
Sign a token on your server
The secret's environment variable name is yours to choose. The examples use WIDGET_SIGNING_SECRET.
Next.js
// 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 });
}Express
// routes/widget-token.js
const jwt = require("jsonwebtoken");
router.get("/widget-token", requireAuth, (req, res) => {
const token = jwt.sign(
{
sub: req.user.id,
email: req.user.email,
name: req.user.name,
},
process.env.WIDGET_SIGNING_SECRET,
{ algorithm: "HS256", expiresIn: "5m" }
);
res.json({ ssoToken: token });
});Django
# views.py
import jwt
import time
from django.conf import settings
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
@login_required
def widget_token(request):
payload = {
"sub": str(request.user.id),
"email": request.user.email,
"name": request.user.get_full_name(),
"exp": int(time.time()) + 300, # 5 minutes
}
token = jwt.encode(payload, settings.WIDGET_SIGNING_SECRET, algorithm="HS256")
return JsonResponse({"ssoToken": token})Rails
# app/controllers/widget_tokens_controller.rb
require "jwt"
class WidgetTokensController < ApplicationController
before_action :authenticate_user!
def show
payload = {
sub: current_user.id.to_s,
email: current_user.email,
name: current_user.name,
exp: 5.minutes.from_now.to_i,
}
token = JWT.encode(payload, ENV["WIDGET_SIGNING_SECRET"], "HS256")
render json: { ssoToken: token }
end
endLaravel
// config/services.php
// 'widget' => ['signing_secret' => env('WIDGET_SIGNING_SECRET')],
// app/Http/Controllers/WidgetTokenController.php
use Firebase\JWT\JWT;
class WidgetTokenController extends Controller
{
public function show(Request $request)
{
$payload = [
'sub' => (string) $request->user()->id,
'email' => $request->user()->email,
'name' => $request->user()->name,
'exp' => time() + 300, // 5 minutes
];
$token = JWT::encode($payload, config('services.widget.signing_secret'), 'HS256');
return response()->json(['ssoToken' => $token]);
}
}Protect the Laravel route with your auth middleware so only signed-in users get a token.
Tip: Mint a fresh token on each login or page load. A short expiry limits the damage if a token is intercepted.
Identify at init
If you already have a token when the widget loads, pass it to init and skip the separate identify call:
Quackback.init({
instanceUrl: "https://feedback.example.com",
identity: { ssoToken },
});Omit identity for anonymous visitors, or if you will call identify once your sign-in check finishes.
Anonymous visitors
Do nothing. After init, the launcher is visible and the visitor is anonymous:
Quackback.init({ instanceUrl: "https://feedback.example.com" });There is no anonymous flag to pass. If the visitor later signs in and you call identify, their anonymous activity merges into 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 the identity. The widget stays visible for anonymous use, and the panel closes if it was open.
Quackback.logout();Custom attributes
Add claims such as plan, company, or mrr to the signed JWT. Matching attributes show on the user's profile and can be used in segments.
// Server-side payload
const payload = {
sub: user.id,
email: user.email,
name: user.name,
plan: "pro",
company: "Acme Corp",
mrr: 299,
};Define the attributes first in Settings > Users. Unknown keys are ignored, not rejected.
Sign portal users in from the widget
If you run a private portal, an identified widget user can open the portal already signed in, with no password and no second sign-up.
- Identify widget users with
ssoToken(this article). - Turn on Widget sign-in in Settings > Access & Security > Portal access.
They stay a customer on the portal. Workspace teammates can identify in the widget, but going to the portal from the widget does not sign them in: they see the portal sign-in screen (or keep an existing dashboard login). See Control portal access and sign-in.
Rotate the secret
If the signing secret is exposed, regenerate it in Settings > Widget > Install (Regenerate next to the signing secret), then deploy the new value in your app. The old secret stops working immediately.
Warning: Regenerate and deploy together. Signed-in users are not recognized until your server signs tokens with the new secret. The launcher and anonymous visitors are unaffected.
Related articles
Was this helpful?
Your feedback shapes what we write next.
