Install the widget
The widget puts heedback inside your own product — feedback and voting, a
"What's new" changelog, and surveys — with a single <script> tag. No build
step, no npm package. Paste the snippet before your closing </body> and you're
done.
Each snippet is also generated with your real workspace and board IDs in the dashboard (the board Share dialog and Settings), so you can copy an exact, ready-to-paste version from there.
Feedback & voting
Collects posts and votes from inside your app. Sits bottom-left.
<script
src="https://heedback.frugalsaas.com/feedback-widget.js"
data-workspace="your-slug"
data-board="your-board-id"
></script>data-workspace— your workspace slug (required).data-board— the board to collect into (required). Find it in the board's Share dialog.
"What's new" changelog
Shows your published changelog entries in a popover. Sits bottom-right, so it won't collide with the feedback widget.
<script
src="https://heedback.frugalsaas.com/changelog-widget.js"
data-workspace="your-slug"
></script>Surveys
Surveys are delivered by the same feedback widget — if a
survey is targeted to the current user, it appears automatically. You only need
the feedback snippet above (the data-board is optional for survey-only use).
Identifying users (and targeting surveys & changelog entries)
By default votes are tied to an anonymous per-browser ID. To tie them to a real
user — and to target surveys and changelog entries by user traits — set
window.heedbackFeedback before the widget script loads:
<script>
window.heedbackFeedback = {
identify: "user-123",
traits: { plan: "pro", role: "admin" },
};
</script>
<script
src="https://heedback.frugalsaas.com/feedback-widget.js"
data-workspace="your-slug"
data-board="your-board-id"
></script>traits are used to decide which surveys a user sees and are recorded on their
responses, so you can segment results later.
They also drive changelog audience targeting: an entry
with audience rules is shown in the "What's new" widget only to users whose
traits match. Set window.heedbackFeedback before the changelog snippet
too, even if that's the only widget you embed — without traits, a user counts as
having none, and entries requiring a trait to be set won't appear for them.
Verifying who your users are (Startup and up)
Anything in the snippet above is set in the browser, so anyone can edit it. On
its own, identify is a hint: it de-duplicates votes, and nothing more. To make
a vote or a post genuinely attributable to a real customer, your server
signs the identity and the widget passes the signature along.
1. Get your workspace's secret
Settings → Customer identity → Generate secret. It's shown once, so copy it straight into your server's environment — we don't store it and can't show it again. Lost it? Rotate, which issues a new one and stops the old one verifying.
2. Sign the identity on your server
Compute an HMAC-SHA256 over a canonical message built from three fields — the
user id, the email and the name — using the whole secret, including the
hbid_ prefix, as the key.
The message is v1|id|email|name, where each field is escaped before joining:
first every \ becomes \\, then every | becomes \|. That escaping is
what keeps the fields unambiguous, so a value containing a | can't be read as
two fields. An absent email or name is an empty field, never a dropped one:
an id-only call signs v1|user-123||.
import crypto from "node:crypto";
const esc = (s) =>
String(s ?? "")
.replace(/\\/g, "\\\\")
.replace(/\|/g, "\\|");
const message = ["v1", esc(id), esc(email), esc(name)].join("|");
const hash = crypto.createHmac("sha256", secret).update(message).digest("hex");import hmac, hashlib
esc = lambda s: (s or "").replace("\\", "\\\\").replace("|", "\\|")
message = "|".join(["v1", esc(user_id), esc(email), esc(name)])
hash = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()The result is 64 lowercase hex characters. Never compute it in the browser — that would hand every visitor the secret.
3. Pass it to the widget
<script>
window.heedbackFeedback = {
identify: "user-123",
name: "Jo Ray",
email: "jo@acme.io",
hash: "…the 64-character hash your server computed…",
traits: { plan: "pro" },
};
</script>Send the same id, email and name you signed. If any of them differs, the signature won't match.
What verification changes
A verified call creates (or updates) a person in your People list, and stamps that person on the post or vote. You'll see a "Verified" badge on the post, you can filter boards and People by traits, and one person gets one vote per post across all their devices.
An unverified call — wrong hash, missing hash, or no secret yet — still works exactly as before: the vote counts, the post is created, it just isn't attributed to anyone, and the email is not stored. Nothing fails, and your users see no difference.
Rejecting unsigned calls
The "Reject calls that aren't signed" switch on the same Settings card
tightens that. With it on, an unsigned or wrongly-signed identify() has its
id, name and email discarded rather than stored unverified — so nobody can post
under a colleague's name by editing the page. Leave it off while you're rolling
the signing code out, and turn it on once every call is signed.
Rules worth knowing
- Omitting a field never erases it. A later call carrying only the id keeps
the stored name, email and traits. Passing a non-empty
traitsobject replaces the stored traits wholesale; an empty one leaves them alone. - Traits are self-reported. They're not covered by the signature, and the dashboard labels them as such. Use them for segmenting, not for anything you need to be able to prove.
- An id longer than 128 characters is rejected, not shortened — shortening could quietly merge two of your users into one person.
- Rotating the secret invalidates every hash computed with the old one. Calls degrade to unverified until your server is signing with the new secret; nothing errors.