@heedb/web-sdk 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +451 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# @heedb/web-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A lightweight (~12 KB) 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.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Dashboard](https://heedb.com) · [GitHub](https://github.com/TheAleSch/heedb)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
### CDN (recommended)
|
|
12
|
+
|
|
13
|
+
The fastest way — no build step, no dependencies. Add one line before `</body>`:
|
|
6
14
|
|
|
7
15
|
```html
|
|
8
16
|
<script
|
|
@@ -12,7 +20,11 @@ Drop-in feedback widget with email conversations for any website.
|
|
|
12
20
|
></script>
|
|
13
21
|
```
|
|
14
22
|
|
|
15
|
-
|
|
23
|
+
> **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.
|
|
24
|
+
|
|
25
|
+
A floating chat button appears in the bottom-right corner. That's it.
|
|
26
|
+
|
|
27
|
+
### npm
|
|
16
28
|
|
|
17
29
|
```bash
|
|
18
30
|
npm install @heedb/web-sdk
|
|
@@ -21,29 +33,452 @@ npm install @heedb/web-sdk
|
|
|
21
33
|
```ts
|
|
22
34
|
import { Heedb } from "@heedb/web-sdk";
|
|
23
35
|
|
|
36
|
+
Heedb.init({ apiKey: "YOUR_API_KEY" });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This dynamically loads the widget and attaches it to the page. Works with React, Next.js, Vue, Svelte, or any framework.
|
|
40
|
+
|
|
41
|
+
### Self-hosted
|
|
42
|
+
|
|
43
|
+
If you run your own Heedb instance, serve `widget.js` from your domain:
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<script src="https://your-instance.com/widget.js" data-api-key="YOUR_API_KEY"></script>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The widget auto-detects the API host from the script's origin — no `data-host` needed.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Environment variables
|
|
54
|
+
|
|
55
|
+
Add these to your `.env` (or equivalent):
|
|
56
|
+
|
|
57
|
+
```env
|
|
58
|
+
# Public — safe for client-side code, used in data-api-key or Heedb.init()
|
|
59
|
+
NEXT_PUBLIC_HEEDB_API_KEY=your_api_key_here
|
|
60
|
+
|
|
61
|
+
# Private — server-side only, used to generate userHash
|
|
62
|
+
# NEVER expose this in client-side code, bundle, or git
|
|
63
|
+
HEEDB_WIDGET_SECRET=your_widget_secret_here
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Both values are in your [dashboard settings](https://heedb.com/app/settings).
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Identify users
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
There are three levels:
|
|
75
|
+
|
|
76
|
+
### 1. Anonymous (default)
|
|
77
|
+
|
|
78
|
+
No `init()` call needed. The widget shows the full contact form. Good for marketing sites, landing pages, or anywhere users aren't logged in.
|
|
79
|
+
|
|
80
|
+
### 2. Identified (name + email)
|
|
81
|
+
|
|
82
|
+
Pre-fills the form and hides the name/email fields. The user only sees the message box. **No server-side code required.**
|
|
83
|
+
|
|
84
|
+
```ts
|
|
24
85
|
Heedb.init({
|
|
25
86
|
apiKey: "YOUR_API_KEY",
|
|
26
|
-
email:
|
|
27
|
-
name:
|
|
28
|
-
userHash: serverHash, // optional — HMAC-SHA256 for verified identity
|
|
87
|
+
email: "jane@example.com",
|
|
88
|
+
name: "Jane",
|
|
29
89
|
});
|
|
30
90
|
```
|
|
31
91
|
|
|
32
|
-
|
|
92
|
+
Use this when you know the user's identity but don't need to show them their conversation history.
|
|
33
93
|
|
|
34
|
-
|
|
94
|
+
### 3. Verified (name + email + userHash)
|
|
95
|
+
|
|
96
|
+
Same as identified, plus unlocks the **Messages** tab where the user can view their previous conversation threads.
|
|
97
|
+
|
|
98
|
+
**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.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// Client-side — after receiving userHash from your server
|
|
102
|
+
Heedb.init({
|
|
103
|
+
apiKey: "YOUR_API_KEY",
|
|
104
|
+
email: "jane@example.com",
|
|
105
|
+
name: "Jane",
|
|
106
|
+
userHash: serverGeneratedHash, // from your backend
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**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).
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Generate the userHash (server-side)
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
118
|
+
**The flow:**
|
|
119
|
+
1. User logs in to your app
|
|
120
|
+
2. Your server computes `HMAC-SHA256(email, WIDGET_SECRET)` → `userHash`
|
|
121
|
+
3. Your server passes `userHash` to the frontend (via props, API response, or server-rendered HTML)
|
|
122
|
+
4. The frontend calls `Heedb.init({ email, userHash })`
|
|
123
|
+
|
|
124
|
+
**Never compute the hash client-side — the Widget Secret must stay on your server.**
|
|
125
|
+
|
|
126
|
+
### Node.js
|
|
35
127
|
|
|
36
128
|
```js
|
|
37
129
|
const crypto = require("crypto");
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
130
|
+
|
|
131
|
+
function heedbUserHash(email) {
|
|
132
|
+
return crypto
|
|
133
|
+
.createHmac("sha256", process.env.HEEDB_WIDGET_SECRET)
|
|
134
|
+
.update(email)
|
|
135
|
+
.digest("hex");
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Python
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
import hmac, hashlib, os
|
|
143
|
+
|
|
144
|
+
def heedb_user_hash(email: str) -> str:
|
|
145
|
+
secret = os.environ["HEEDB_WIDGET_SECRET"].encode()
|
|
146
|
+
return hmac.new(secret, email.encode(), hashlib.sha256).hexdigest()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### PHP
|
|
150
|
+
|
|
151
|
+
```php
|
|
152
|
+
function heedbUserHash(string $email): string {
|
|
153
|
+
return hash_hmac('sha256', $email, getenv('HEEDB_WIDGET_SECRET'));
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Ruby
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
require "openssl"
|
|
161
|
+
|
|
162
|
+
def heedb_user_hash(email)
|
|
163
|
+
OpenSSL::HMAC.hexdigest("sha256", ENV["HEEDB_WIDGET_SECRET"], email)
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Go
|
|
168
|
+
|
|
169
|
+
```go
|
|
170
|
+
import (
|
|
171
|
+
"crypto/hmac"
|
|
172
|
+
"crypto/sha256"
|
|
173
|
+
"encoding/hex"
|
|
174
|
+
"os"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
func heedbUserHash(email string) string {
|
|
178
|
+
h := hmac.New(sha256.New, []byte(os.Getenv("HEEDB_WIDGET_SECRET")))
|
|
179
|
+
h.Write([]byte(email))
|
|
180
|
+
return hex.EncodeToString(h.Sum(nil))
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Framework examples
|
|
187
|
+
|
|
188
|
+
### Next.js (App Router) — full example with verified identity
|
|
189
|
+
|
|
190
|
+
This is the recommended pattern. The hash is computed in a server component and passed to a client component.
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
// app/components/HeedbWidget.server.tsx — Server Component
|
|
194
|
+
import { auth } from "@/lib/auth"; // your auth library
|
|
195
|
+
import { headers } from "next/headers";
|
|
196
|
+
import { createHmac } from "crypto";
|
|
197
|
+
import HeedbWidgetClient from "./HeedbWidget.client";
|
|
198
|
+
|
|
199
|
+
export default async function HeedbWidget() {
|
|
200
|
+
let email: string | undefined;
|
|
201
|
+
let name: string | undefined;
|
|
202
|
+
let userHash: string | undefined;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
206
|
+
if (session?.user?.email) {
|
|
207
|
+
email = session.user.email;
|
|
208
|
+
name = session.user.name ?? undefined;
|
|
209
|
+
// Compute HMAC on the server — secret never reaches the client
|
|
210
|
+
userHash = createHmac("sha256", process.env.HEEDB_WIDGET_SECRET!)
|
|
211
|
+
.update(email)
|
|
212
|
+
.digest("hex");
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// No session — widget will work anonymously
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return <HeedbWidgetClient email={email} name={name} userHash={userHash} />;
|
|
219
|
+
}
|
|
42
220
|
```
|
|
43
221
|
|
|
44
|
-
|
|
222
|
+
```tsx
|
|
223
|
+
// app/components/HeedbWidget.client.tsx — Client Component
|
|
224
|
+
"use client";
|
|
225
|
+
|
|
226
|
+
import { useEffect } from "react";
|
|
227
|
+
import { Heedb } from "@heedb/web-sdk";
|
|
228
|
+
|
|
229
|
+
export default function HeedbWidgetClient({ email, name, userHash }: {
|
|
230
|
+
email?: string;
|
|
231
|
+
name?: string;
|
|
232
|
+
userHash?: string;
|
|
233
|
+
}) {
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
Heedb.init({
|
|
236
|
+
apiKey: process.env.NEXT_PUBLIC_HEEDB_API_KEY!,
|
|
237
|
+
email,
|
|
238
|
+
name,
|
|
239
|
+
userHash,
|
|
240
|
+
});
|
|
241
|
+
}, [email, name, userHash]);
|
|
242
|
+
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
// app/layout.tsx
|
|
249
|
+
import HeedbWidget from "./components/HeedbWidget.server";
|
|
250
|
+
|
|
251
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
252
|
+
return (
|
|
253
|
+
<html>
|
|
254
|
+
<body>
|
|
255
|
+
{children}
|
|
256
|
+
<HeedbWidget />
|
|
257
|
+
</body>
|
|
258
|
+
</html>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Next.js (Pages Router)
|
|
264
|
+
|
|
265
|
+
Compute the hash in `getServerSideProps` and pass it as a prop:
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
// pages/_app.tsx
|
|
269
|
+
import { useEffect } from "react";
|
|
270
|
+
import { Heedb } from "@heedb/web-sdk";
|
|
271
|
+
|
|
272
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (pageProps.heedbEmail) {
|
|
275
|
+
Heedb.init({
|
|
276
|
+
apiKey: process.env.NEXT_PUBLIC_HEEDB_API_KEY!,
|
|
277
|
+
email: pageProps.heedbEmail,
|
|
278
|
+
name: pageProps.heedbName,
|
|
279
|
+
userHash: pageProps.heedbUserHash,
|
|
280
|
+
});
|
|
281
|
+
} else {
|
|
282
|
+
Heedb.init({ apiKey: process.env.NEXT_PUBLIC_HEEDB_API_KEY! });
|
|
283
|
+
}
|
|
284
|
+
}, [pageProps.heedbEmail]);
|
|
285
|
+
|
|
286
|
+
return <Component {...pageProps} />;
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### React (Vite / CRA) — with API route for hash
|
|
291
|
+
|
|
292
|
+
When you don't have server components, fetch the hash from an API endpoint:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
// Server: POST /api/heedb-hash
|
|
296
|
+
import crypto from "crypto";
|
|
297
|
+
export function handler(req, res) {
|
|
298
|
+
const { email } = req.body;
|
|
299
|
+
const hash = crypto
|
|
300
|
+
.createHmac("sha256", process.env.HEEDB_WIDGET_SECRET!)
|
|
301
|
+
.update(email)
|
|
302
|
+
.digest("hex");
|
|
303
|
+
res.json({ userHash: hash });
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
// Client: App.tsx
|
|
309
|
+
import { useEffect } from "react";
|
|
310
|
+
import { Heedb } from "@heedb/web-sdk";
|
|
311
|
+
|
|
312
|
+
function App() {
|
|
313
|
+
const user = useAuth(); // your auth hook
|
|
314
|
+
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
if (!user) {
|
|
317
|
+
Heedb.init({ apiKey: import.meta.env.VITE_HEEDB_API_KEY });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Fetch the hash from your server
|
|
322
|
+
fetch("/api/heedb-hash", {
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers: { "Content-Type": "application/json" },
|
|
325
|
+
body: JSON.stringify({ email: user.email }),
|
|
326
|
+
})
|
|
327
|
+
.then((r) => r.json())
|
|
328
|
+
.then(({ userHash }) => {
|
|
329
|
+
Heedb.init({
|
|
330
|
+
apiKey: import.meta.env.VITE_HEEDB_API_KEY,
|
|
331
|
+
email: user.email,
|
|
332
|
+
name: user.name,
|
|
333
|
+
userHash,
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
}, [user]);
|
|
337
|
+
|
|
338
|
+
return <div>Your app</div>;
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Vue
|
|
343
|
+
|
|
344
|
+
```vue
|
|
345
|
+
<script setup>
|
|
346
|
+
import { onMounted } from "vue";
|
|
347
|
+
import { Heedb } from "@heedb/web-sdk";
|
|
348
|
+
|
|
349
|
+
const props = defineProps<{
|
|
350
|
+
email?: string;
|
|
351
|
+
name?: string;
|
|
352
|
+
userHash?: string;
|
|
353
|
+
}>();
|
|
354
|
+
|
|
355
|
+
onMounted(() => {
|
|
356
|
+
Heedb.init({
|
|
357
|
+
apiKey: import.meta.env.VITE_HEEDB_API_KEY,
|
|
358
|
+
email: props.email,
|
|
359
|
+
name: props.name,
|
|
360
|
+
userHash: props.userHash,
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
</script>
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Svelte
|
|
367
|
+
|
|
368
|
+
```svelte
|
|
369
|
+
<script>
|
|
370
|
+
import { onMount } from "svelte";
|
|
371
|
+
import { Heedb } from "@heedb/web-sdk";
|
|
372
|
+
|
|
373
|
+
export let email = undefined;
|
|
374
|
+
export let name = undefined;
|
|
375
|
+
export let userHash = undefined;
|
|
376
|
+
|
|
377
|
+
onMount(() => {
|
|
378
|
+
Heedb.init({
|
|
379
|
+
apiKey: import.meta.env.VITE_HEEDB_API_KEY,
|
|
380
|
+
email,
|
|
381
|
+
name,
|
|
382
|
+
userHash,
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
</script>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Static HTML / WordPress / Webflow
|
|
389
|
+
|
|
390
|
+
Use the CDN script tag — no build tools needed:
|
|
391
|
+
|
|
392
|
+
```html
|
|
393
|
+
<script
|
|
394
|
+
src="https://cdn.jsdelivr.net/npm/@heedb/web-sdk/dist/widget.js"
|
|
395
|
+
data-api-key="YOUR_API_KEY"
|
|
396
|
+
data-host="https://heedb.com"
|
|
397
|
+
></script>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
For verified identity on static sites, you'll need a small server endpoint that returns the `userHash` — see the React + API route example above.
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## What the widget does
|
|
405
|
+
|
|
406
|
+
The widget adds a floating button to your page with three tabs:
|
|
407
|
+
|
|
408
|
+
| Tab | Description | Requires |
|
|
409
|
+
|-----|-------------|----------|
|
|
410
|
+
| **Message** | Contact form — name, email, and a message. Creates a new support thread. Fields are hidden when the user is identified. | Nothing |
|
|
411
|
+
| **Privacy** | GDPR/privacy request form — data export, deletion, or opt-out. | Nothing |
|
|
412
|
+
| **Messages** | Conversation history — shows open threads. | Verified identity (`userHash`) |
|
|
413
|
+
|
|
414
|
+
**The full conversation loop:**
|
|
415
|
+
|
|
416
|
+
1. Customer submits a message via the widget
|
|
417
|
+
2. A thread appears in your [Heedb dashboard](https://heedb.com/app/threads)
|
|
418
|
+
3. You get an email notification
|
|
419
|
+
4. You reply from the dashboard — the customer receives an email
|
|
420
|
+
5. The customer replies to that email — it shows up in the dashboard
|
|
421
|
+
6. If the customer is verified, they can also see the full conversation in the widget's Messages tab
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Configuration reference
|
|
426
|
+
|
|
427
|
+
### Script tag attributes
|
|
428
|
+
|
|
429
|
+
| Attribute | Required | Description |
|
|
430
|
+
|-----------|----------|-------------|
|
|
431
|
+
| `data-api-key` | Yes | Your project's public API key |
|
|
432
|
+
| `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. |
|
|
433
|
+
|
|
434
|
+
### `Heedb.init()` options
|
|
435
|
+
|
|
436
|
+
| Option | Type | Required | Description |
|
|
437
|
+
|--------|------|----------|-------------|
|
|
438
|
+
| `apiKey` | `string` | npm: Yes. Script tag: No | Your project's public API key. Script tag reads it from `data-api-key`. |
|
|
439
|
+
| `host` | `string` | No | API host URL. Defaults to `https://heedb.com`. |
|
|
440
|
+
| `email` | `string` | No | User's email — pre-fills the form and hides the email field |
|
|
441
|
+
| `name` | `string` | No | User's name — pre-fills the form and hides the name field |
|
|
442
|
+
| `userHash` | `string` | No | Server-generated HMAC-SHA256 hash — unlocks conversation history. **Must come from your server.** |
|
|
443
|
+
|
|
444
|
+
### Behavior by identity level
|
|
445
|
+
|
|
446
|
+
| What happens | Anonymous | Identified | Verified |
|
|
447
|
+
|---|---|---|---|
|
|
448
|
+
| `init()` called | No | Yes (email + name) | Yes (email + name + userHash) |
|
|
449
|
+
| Contact form visible | Yes (full form) | Yes (message only) | Yes (message only) |
|
|
450
|
+
| Privacy tab visible | Yes | Yes | Yes |
|
|
451
|
+
| Messages tab visible | No | No | Yes |
|
|
452
|
+
| Name/email fields | Shown | Hidden | Hidden |
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Security
|
|
457
|
+
|
|
458
|
+
| Credential | Where it lives | Purpose |
|
|
459
|
+
|------------|---------------|---------|
|
|
460
|
+
| **API Key** (`NEXT_PUBLIC_HEEDB_API_KEY`) | Client-side (public) | Identifies your project. Safe to embed in frontend code. |
|
|
461
|
+
| **Widget Secret** (`HEEDB_WIDGET_SECRET`) | Server-side only (private) | Signs the `userHash`. **Never expose in client code, bundles, or git.** |
|
|
462
|
+
|
|
463
|
+
- The API key controls which project receives the submission — it doesn't grant read access to any data
|
|
464
|
+
- 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
|
|
465
|
+
- Restrict which domains can use your API key under **Settings > Allowed Domains** in the dashboard
|
|
466
|
+
- If no allowed domains are configured, the widget works from any origin (useful for development)
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Troubleshooting
|
|
471
|
+
|
|
472
|
+
| Problem | Cause | Fix |
|
|
473
|
+
|---------|-------|-----|
|
|
474
|
+
| Widget doesn't appear | Missing `data-api-key` or invalid API key | Check the key in your [dashboard settings](https://heedb.com/app/settings) |
|
|
475
|
+
| "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 |
|
|
476
|
+
| "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 |
|
|
477
|
+
| 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` |
|
|
478
|
+
| Messages tab not visible | User is identified but not verified | Pass `userHash` from your server to enable the Messages tab |
|
|
479
|
+
|
|
480
|
+
---
|
|
45
481
|
|
|
46
|
-
##
|
|
482
|
+
## License
|
|
47
483
|
|
|
48
|
-
|
|
49
|
-
- [GitHub](https://github.com/TheAleSch/heedb)
|
|
484
|
+
MIT
|