Blog ·
Sign your users in to an embedded widget with one signed token
How Ozvi knows who is reporting a bug without a second login: a short-lived HS256 token from your backend, in Node.js, Python or PHP, and what to put in it.
A feedback widget is only useful if it knows who is talking. “Someone” reported a bug on the invoice page is a dead end; “Sarah from Pilates House, on the Pro plan” is a conversation. But nobody wants to log in a second time to report a bug, and you don't want a third-party account system next to yours.
Ozvi solves it the way most embedded tools should: your backend, which already knows who is signed in, signs a short statement (“this is user u-42, sarah@…, role member”) and the widget passes it along. Ozvi checks the signature and trusts it. No redirect, no password, no Ozvi account for your users.
The whole flow
- The widget calls an endpoint in your app, the
token-url. - Your endpoint checks your own session and returns
{ "token": "…" }. - The widget sends the token with every request to Ozvi and caches it until shortly before it expires.
<script src="https://feedback.your-app.com/widget.js" defer></script>
<ozvi-widget project="your-app" token-url="/api/ozvi-token"></ozvi-widget>The secret that signs the token is shared between your server and Ozvi (board settings → Sign-in). It never goes to the browser.
The token is a plain JWT
No proprietary format: a JSON Web Token signed with HMAC-SHA256 (HS256), valid for ten minutes. That means any language can make one with its standard library.
| Field | What it is |
|---|---|
id | Required. The person's stable id in your app. Ozvi keys on it, never on the e-mail alone. |
email | Optional. Used for replies (“fixed in 3.9.2, does it work for you?”). |
name | Optional. Shown to your team. |
role | member (default), team (can triage) or admin (can change settings). |
traits | Optional, team-only: company, plan, mrr in cents… so you can sort ideas by revenue. |
iat, exp | Issued at and expiry, in seconds. Keep it short-lived. |
Node.js
With the MIT-licensed SDK (no dependencies):
import { signOzviToken } from "@ozvi/sdk";
app.get("/api/ozvi-token", requireLogin, (req, res) => {
res.json({ token: signOzviToken(
{ id: req.user.id, email: req.user.email, name: req.user.name,
traits: { company: req.org.name, plan: req.org.plan } },
process.env.OZVI_SSO_SECRET) });
});In Next.js the same goes into app/api/ozvi-token/route.ts:
import { signOzviToken } from "@ozvi/sdk";
import { auth } from "@/auth"; // your session
export async function GET() {
const session = await auth();
if (!session?.user) return new Response(null, { status: 401 });
const { id, email, name } = session.user;
return Response.json({ token: signOzviToken({ id, email, name }, process.env.OZVI_SSO_SECRET!) });
}Python (standard library only)
import base64, hashlib, hmac, json, os, time
def ozvi_token(user: dict, secret: str = os.environ["OZVI_SSO_SECRET"]) -> str:
b64 = lambda o: base64.urlsafe_b64encode(json.dumps(o, separators=(",", ":")).encode()).rstrip(b"=").decode()
now = int(time.time())
body = f'{b64({"alg": "HS256", "typ": "JWT"})}.{b64({**user, "iat": now, "exp": now + 600})}'
sig = hmac.new(secret.encode(), body.encode(), hashlib.sha256).digest()
return f"{body}.{base64.urlsafe_b64encode(sig).rstrip(b'=').decode()}"PHP (no Composer package)
function ozvi_token(array $user, ?string $secret = null): string {
$secret ??= getenv('OZVI_SSO_SECRET');
$b64 = fn($s) => rtrim(strtr(base64_encode($s), '+/', '-_'), '=');
$now = time();
$body = $b64(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])) . '.'
. $b64(json_encode($user + ['iat' => $now, 'exp' => $now + 600], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
return $body . '.' . $b64(hash_hmac('sha256', $body, $secret, true));
}All three produce tokens that Ozvi's own verifier accepts; we test them against it.
Getting it right
- Keep it short-lived. Ten minutes is plenty, the widget asks for a new one when it needs it. A leaked token is useless soon after.
- Sign only for signed-in users. Check your own session first and return 401 without it; never sign whatever the browser sends you.
- Use your stable id, not the e-mail, as
id. People change e-mails. - Roles come from you. Give
teamonly to your staff: they see every report and the customer data intraits. - Don't paste real tokens into online decoders. They carry your users' e-mails.
More