npm package: @heedb/web-sdk
Description: Drop-in feedback widget with email conversations for any website
Latest version: 1.1.7

# @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.

[Dashboard](https://heedb.com) · [GitHub](https://github.com/TheAleSch/heedb)

---

## Quick start

### CDN (recommended)

The fastest way — no build step, no dependencies. Add one line before `</body>`:

```html
<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-host` is 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](https://heedb.com/app/customization).

### npm

```bash
npm install @heedb/web-sdk
```

```ts
import { 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:

```html
<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):

```env
# 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_here
```

Both values are in your [dashboard settings](https://heedb.com/app/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.**

```ts
Heedb.init({
  apiKey: "YOUR_API_KEY",
  email: "jane@example.com",
  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.

```ts
// Client-side — after receiving userHash from your server
Heedb.init({
  apiKey: "YOUR_API_KEY",
  email: "jane@example.com",
  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:**
1. User logs in to your app
2. Your server computes `HMAC-SHA256(email, WIDGET_SECRET)` → `userHash`
3. Your server passes `userHash` to the frontend (via props, API response, or server-rendered HTML)
4. The frontend calls `Heedb.init({ email, userHash })`

**Never compute the hash client-side — the Widget Secret must stay on your server.**

### Node.js

```js
const crypto = require("crypto");

function heedbUserHash(email) {
  return crypto
    .createHmac("sha256", process.env.HEEDB_WIDGET_SECRET)
    .update(email)
    .digest("hex");
}
```

### Python

```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

```php
function heedbUserHash(string $email): string {
    return hash_hmac('sha256', $email, getenv('HEEDB_WIDGET_SECRET'));
}
```

### Ruby

```ruby
require "openssl"

def heedb_user_hash(email)
  OpenSSL::HMAC.hexdigest("sha256", ENV["HEEDB_WIDGET_SECRET"], email)
end
```

### Go

```go
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.

```tsx
// 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} />;
}
```

```tsx
// 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;
}
```

```tsx
// 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:

```tsx
// 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:

```ts
// 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 });
}
```

```tsx
// 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

```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

```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:

```html
<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:**

1. Customer submits a message via the widget
2. A thread appears in your [Heedb dashboard](https://heedb.com/app/threads)
3. You get an email notification
4. You reply from the dashboard — the customer receives an email
5. The customer replies to that email — it shows up in the dashboard
6. 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](https://heedb.com/app/customization) — 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:

```ts
Heedb.init({
  apiKey: "YOUR_API_KEY",
  lang: "es", // "en" | "es" | "pt"
});
```

Or via the script tag:

```html
<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.

```ts
// 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 `userHash` is 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](https://heedb.com/app/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