npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@heedb/web-sdk

v1.1.7

Published

Drop-in feedback widget with email conversations for any website

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.

Dashboard · GitHub


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

npm

npm install @heedb/web-sdk
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:

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

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

  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

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)
end

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.

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

  1. Customer submits a message via the widget
  2. A thread appears in your Heedb dashboard
  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 — 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 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 | | "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