@heedb/web-sdk
v1.1.7
Published
Drop-in feedback widget with email conversations for any website
Maintainers
Readme
@heedb/web-sdk
A lightweight feedback widget that drops into any website. Customers can send messages, make privacy requests, and view their conversation history — all through email-based threads. No login required.
Quick start
CDN (recommended)
The fastest way — no build step, no dependencies. Add one line before </body>:
<script
src="https://cdn.jsdelivr.net/npm/@heedb/web-sdk@1/widget.js"
data-api-key="YOUR_API_KEY"
data-host="https://heedb.com"
></script>Important:
data-hostis required when loading from a CDN. It tells the widget where to send API requests. Omit it only when the script is served from the same domain as your Heedb instance.
A floating chat button appears in the bottom-right corner. That's it. Customize the look, trigger mode, and behavior from your dashboard.
npm
npm install @heedb/web-sdkimport { Heedb } from "@heedb/web-sdk";
Heedb.init({ apiKey: "YOUR_API_KEY" });This dynamically loads the widget and attaches it to the page. Works with React, Next.js, Vue, Svelte, or any framework.
Self-hosted
If you run your own Heedb instance, serve widget.js from your domain:
<script src="https://your-instance.com/widget.js" data-api-key="YOUR_API_KEY"></script>The widget auto-detects the API host from the script's origin — no data-host needed.
Environment variables
Add these to your .env (or equivalent):
# Public — safe for client-side code, used in data-api-key or Heedb.init()
NEXT_PUBLIC_HEEDB_API_KEY=your_api_key_here
# Private — server-side only, used to generate userHash
# NEVER expose this in client-side code, bundle, or git
HEEDB_WIDGET_SECRET=your_widget_secret_hereBoth values are in your dashboard settings.
Identify users
By default, the widget shows a form asking for name, email, and message. If the user is already logged in to your app, you can skip that step.
There are three levels:
1. Anonymous (default)
No init() call needed. The widget shows the full contact form. Good for marketing sites, landing pages, or anywhere users aren't logged in.
2. Identified (name + email)
Pre-fills the form and hides the name/email fields. The user only sees the message box. No server-side code required.
Heedb.init({
apiKey: "YOUR_API_KEY",
email: "[email protected]",
name: "Jane",
});Use this when you know the user's identity but don't need to show them their conversation history.
3. Verified (name + email + userHash)
Same as identified, plus unlocks the Messages tab where the user can view their previous conversation threads.
This requires a server-generated HMAC. The userHash must be computed on your backend and passed to the frontend — never generate it client-side, as that would expose your Widget Secret.
// Client-side — after receiving userHash from your server
Heedb.init({
apiKey: "YOUR_API_KEY",
email: "[email protected]",
name: "Jane",
userHash: serverGeneratedHash, // from your backend
});Only pass userHash when a user is authenticated. For logged-out users, either call init() without it (identified) or don't call init() at all (anonymous).
Generate the userHash (server-side)
The userHash is an HMAC-SHA256 of the user's email, signed with your Widget Secret. It proves your server vouches for this user's identity.
The flow:
- User logs in to your app
- Your server computes
HMAC-SHA256(email, WIDGET_SECRET)→userHash - Your server passes
userHashto the frontend (via props, API response, or server-rendered HTML) - The frontend calls
Heedb.init({ email, userHash })
Never compute the hash client-side — the Widget Secret must stay on your server.
Node.js
const crypto = require("crypto");
function heedbUserHash(email) {
return crypto
.createHmac("sha256", process.env.HEEDB_WIDGET_SECRET)
.update(email)
.digest("hex");
}Python
import hmac, hashlib, os
def heedb_user_hash(email: str) -> str:
secret = os.environ["HEEDB_WIDGET_SECRET"].encode()
return hmac.new(secret, email.encode(), hashlib.sha256).hexdigest()PHP
function heedbUserHash(string $email): string {
return hash_hmac('sha256', $email, getenv('HEEDB_WIDGET_SECRET'));
}Ruby
require "openssl"
def heedb_user_hash(email)
OpenSSL::HMAC.hexdigest("sha256", ENV["HEEDB_WIDGET_SECRET"], email)
endGo
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"os"
)
func heedbUserHash(email string) string {
h := hmac.New(sha256.New, []byte(os.Getenv("HEEDB_WIDGET_SECRET")))
h.Write([]byte(email))
return hex.EncodeToString(h.Sum(nil))
}Framework examples
Next.js (App Router) — full example with verified identity
This is the recommended pattern. The hash is computed in a server component and passed to a client component.
// app/components/HeedbWidget.server.tsx — Server Component
import { auth } from "@/lib/auth"; // your auth library
import { headers } from "next/headers";
import { createHmac } from "crypto";
import HeedbWidgetClient from "./HeedbWidget.client";
export default async function HeedbWidget() {
let email: string | undefined;
let name: string | undefined;
let userHash: string | undefined;
try {
const session = await auth.api.getSession({ headers: await headers() });
if (session?.user?.email) {
email = session.user.email;
name = session.user.name ?? undefined;
// Compute HMAC on the server — secret never reaches the client
userHash = createHmac("sha256", process.env.HEEDB_WIDGET_SECRET!)
.update(email)
.digest("hex");
}
} catch {
// No session — widget will work anonymously
}
return <HeedbWidgetClient email={email} name={name} userHash={userHash} />;
}// app/components/HeedbWidget.client.tsx — Client Component
"use client";
import { useEffect } from "react";
import { Heedb } from "@heedb/web-sdk";
export default function HeedbWidgetClient({ email, name, userHash }: {
email?: string;
name?: string;
userHash?: string;
}) {
useEffect(() => {
Heedb.init({
apiKey: process.env.NEXT_PUBLIC_HEEDB_API_KEY!,
email,
name,
userHash,
});
}, [email, name, userHash]);
return null;
}// app/layout.tsx
import HeedbWidget from "./components/HeedbWidget.server";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<HeedbWidget />
</body>
</html>
);
}Next.js (Pages Router)
Compute the hash in getServerSideProps and pass it as a prop:
// pages/_app.tsx
import { useEffect } from "react";
import { Heedb } from "@heedb/web-sdk";
export default function App({ Component, pageProps }: AppProps) {
useEffect(() => {
if (pageProps.heedbEmail) {
Heedb.init({
apiKey: process.env.NEXT_PUBLIC_HEEDB_API_KEY!,
email: pageProps.heedbEmail,
name: pageProps.heedbName,
userHash: pageProps.heedbUserHash,
});
} else {
Heedb.init({ apiKey: process.env.NEXT_PUBLIC_HEEDB_API_KEY! });
}
}, [pageProps.heedbEmail]);
return <Component {...pageProps} />;
}React (Vite / CRA) — with API route for hash
When you don't have server components, fetch the hash from an API endpoint:
// Server: POST /api/heedb-hash
import crypto from "crypto";
export function handler(req, res) {
const { email } = req.body;
const hash = crypto
.createHmac("sha256", process.env.HEEDB_WIDGET_SECRET!)
.update(email)
.digest("hex");
res.json({ userHash: hash });
}// Client: App.tsx
import { useEffect } from "react";
import { Heedb } from "@heedb/web-sdk";
function App() {
const user = useAuth(); // your auth hook
useEffect(() => {
if (!user) {
Heedb.init({ apiKey: import.meta.env.VITE_HEEDB_API_KEY });
return;
}
// Fetch the hash from your server
fetch("/api/heedb-hash", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: user.email }),
})
.then((r) => r.json())
.then(({ userHash }) => {
Heedb.init({
apiKey: import.meta.env.VITE_HEEDB_API_KEY,
email: user.email,
name: user.name,
userHash,
});
});
}, [user]);
return <div>Your app</div>;
}Vue
<script setup>
import { onMounted } from "vue";
import { Heedb } from "@heedb/web-sdk";
const props = defineProps<{
email?: string;
name?: string;
userHash?: string;
}>();
onMounted(() => {
Heedb.init({
apiKey: import.meta.env.VITE_HEEDB_API_KEY,
email: props.email,
name: props.name,
userHash: props.userHash,
});
});
</script>Svelte
<script>
import { onMount } from "svelte";
import { Heedb } from "@heedb/web-sdk";
export let email = undefined;
export let name = undefined;
export let userHash = undefined;
onMount(() => {
Heedb.init({
apiKey: import.meta.env.VITE_HEEDB_API_KEY,
email,
name,
userHash,
});
});
</script>Static HTML / WordPress / Webflow
Use the CDN script tag — no build tools needed:
<script
src="https://cdn.jsdelivr.net/npm/@heedb/web-sdk@1/widget.js"
data-api-key="YOUR_API_KEY"
data-host="https://heedb.com"
></script>For verified identity on static sites, you'll need a small server endpoint that returns the userHash — see the React + API route example above.
What the widget does
The widget adds a trigger button to your page with a bottom navigation bar containing up to three tabs:
| Tab | Description | Requires |
|-----|-------------|----------|
| Message | Contact form — name, email, and a message. Creates a new support thread. Fields are hidden when the user is identified. | Nothing |
| Privacy | GDPR/privacy request form — data export, deletion, or opt-out. Can be hidden via dashboard settings. | Nothing |
| Messages | Conversation history — shows open threads with status badges (new messages, awaiting reply, closed). | Verified identity (userHash) |
The full conversation loop:
- Customer submits a message via the widget
- A thread appears in your Heedb dashboard
- You get an email notification
- You reply from the dashboard — the customer receives an email
- The customer replies to that email — it shows up in the dashboard
- If the customer is verified, they can also see the full conversation in the widget's Messages tab
Trigger modes
The widget supports three trigger modes, configured in your dashboard under Customization:
| Mode | Description | |------|-------------| | Floating (default) | A round button fixed in the bottom-right or bottom-left corner | | Tag | A vertical tab on the left or right edge of the page (e.g. "Talk to us!") | | Embedded | No visible button — you provide a CSS selector for an existing element on your page that opens the widget when clicked |
All trigger modes support a custom SVG icon or emoji, plus optional button text.
Customization
All visual customization is done through the dashboard — no code changes needed. Available options:
| Option | Description | |--------|-------------| | Theme mode | Light, dark, or system (follows OS preference) | | Colors | Primary, text, and background colors — separate sets for light and dark mode | | Border radius | Panel corner rounding (0–24px) | | Button radius | Trigger button rounding (0–50px / full circle) | | Font family | Custom font stack | | Header title | Custom panel title (e.g. "Talk to us!") | | Trigger icon | Custom SVG icon for the trigger button | | Trigger emoji | Emoji for the trigger (default: chat bubble) | | Trigger text | Label next to the icon (tag mode and floating with label) | | Position | Bottom-right or bottom-left (floating), right or left edge (tag) | | Privacy tab | Show or hide the privacy/GDPR tab | | Custom CSS | Inject custom CSS to override any widget style | | Custom labels | Localized overrides for tab labels, placeholders, and empty state text (en/es/pt) |
Language
The widget auto-detects the page language from the <html lang> attribute and supports English, Spanish, and Portuguese. You can also set it programmatically:
Heedb.init({
apiKey: "YOUR_API_KEY",
lang: "es", // "en" | "es" | "pt"
});Or via the script tag:
<script
src="https://cdn.jsdelivr.net/npm/@heedb/web-sdk@1/widget.js"
data-api-key="YOUR_API_KEY"
data-host="https://heedb.com"
data-lang="es"
></script>API reference
Script tag attributes
| Attribute | Required | Description |
|-----------|----------|-------------|
| data-api-key | Yes | Your project's public API key |
| data-host | CDN: Yes. Self-hosted: No | API host URL. Required when loading from CDN (e.g. jsDelivr). When self-hosted, defaults to the script's origin. |
| data-lang | No | Override language detection (en, es, or pt) |
Heedb.init(options)
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| apiKey | string | npm: Yes. Script tag: No | Your project's public API key. Script tag reads it from data-api-key. |
| host | string | No | API host URL. Defaults to https://heedb.com. |
| email | string | No | User's email — pre-fills the form and hides the email field |
| name | string | No | User's name — pre-fills the form and hides the name field |
| userHash | string | No | Server-generated HMAC-SHA256 hash — unlocks conversation history. Must come from your server. |
| lang | string | No | Override language ("en", "es", or "pt"). Defaults to <html lang> detection. |
Heedb.reset()
Clears the current user's identity and resets the widget to anonymous mode. Call this on logout to prevent the next user from seeing stale data.
// On user logout
Heedb.reset();Behavior by identity level
| What happens | Anonymous | Identified | Verified |
|---|---|---|---|
| init() called | No | Yes (email + name) | Yes (email + name + userHash) |
| Contact form visible | Yes (full form) | Yes (message only) | Yes (message only) |
| Privacy tab visible | Yes (unless hidden in dashboard) | Yes (unless hidden) | Yes (unless hidden) |
| Messages tab visible | No | No | Yes |
| Name/email fields | Shown | Hidden | Hidden |
Security
| Credential | Where it lives | Purpose |
|------------|---------------|---------|
| API Key (NEXT_PUBLIC_HEEDB_API_KEY) | Client-side (public) | Identifies your project. Safe to embed in frontend code. |
| Widget Secret (HEEDB_WIDGET_SECRET) | Server-side only (private) | Signs the userHash. Never expose in client code, bundles, or git. |
- The API key controls which project receives the submission — it doesn't grant read access to any data
- The
userHashis a cryptographic proof that your server vouches for the user's email — without it, users can submit messages but can't read thread history - Restrict which domains can use your API key under Settings > Allowed Domains in the dashboard
- If no allowed domains are configured, the widget works from any origin (useful for development)
Troubleshooting
| Problem | Cause | Fix |
|---------|-------|-----|
| Widget doesn't appear | Missing data-api-key or invalid API key | Check the key in your dashboard settings |
| "Origin not allowed" (403) | Your domain isn't in the project's allowed domains list | Add your domain in Settings > Allowed Domains, or leave the list empty to allow all |
| "Failed to load messages" | userHash is missing, invalid, or computed with the wrong secret | Verify you're using the Widget Secret (not the API key) and computing HMAC-SHA256 server-side |
| Widget loads but init() has no effect | init() called before script loads | With npm: Heedb.init() handles this automatically. With CDN: call init() in the script's onload or after DOMContentLoaded |
| Messages tab not visible | User is identified but not verified | Pass userHash from your server to enable the Messages tab |
License
MIT
