# featurebase-js

Typed JavaScript / TypeScript SDK for [Featurebase](https://featurebase.app).
One package, full coverage for every embeddable surface:

- **Messenger** — in-app chat, help center, changelog feed, news.
- **Changelog widget** — dropdown / popup / floating "What's new" card.
- **Feedback widget** — floating button to browse boards and submit posts.
- **Surveys** — auto-displays active surveys based on dashboard targeting.
- **Embed widget** — embed any Featurebase board / roadmap page directly inside your app.

The runtime script (`https://do.featurebase.app/js/sdk.js`) is auto-injected
on first use — you never need to add a `<script>` tag yourself. Every
export is tree-shakable, fully typed, and SSR-safe.

## Install

```bash
npm  install featurebase-js
pnpm add     featurebase-js
yarn add     featurebase-js
```

Peer dependency: **React ≥ 17** (only required if you import from
`featurebase-js/react`).

---

# 1. Setup

**One field, one call.** Pass your `appId` and the SDK figures
everything out:

- Boots the messenger when your dashboard has **Manage modules →
  Support** enabled (flip the toggle later, no code change needed).
- Sets identity defaults for every standalone widget below (changelog /
  feedback / surveys), so you never repeat them per widget.

Find your `appId` in the dashboard under **Settings → Developers →
Installation**.

## Path A — React

Add **one** Provider at your app root.

```tsx
// app/layout.tsx (Next.js) — must be a Client Component
'use client';

import { FeaturebaseProvider } from 'featurebase-js/react';

export default function RootLayout({ children, user }) {
  return (
    <FeaturebaseProvider
      appId="abc123"                         // Settings → Developers → Installation
      featurebaseJwt={user?.featurebaseJwt}  // Optional — see Section 8
    >
      {children}
    </FeaturebaseProvider>
  );
}
```

> ✅ **Done.** Anonymous visitors are fully supported — `appId` alone is
> enough to render every surface. To attribute conversations / posts /
> survey responses to a logged-in user, see
> [Section 8 — Identifying users](#8-identifying-users-jwt).

> Jump to [Section 2 (Messenger)](#2-messenger) to control it, or
> [Section 3 / 4 / 5 / 6](#3-changelog-widget) to wire up the standalone
> widgets — you don't have to repeat `appId`.

`FeaturebaseProvider` props (full list):

| Prop | Type | Purpose |
|---|---|---|
| `appId` | `string` | Your Featurebase organization id. Drives both messenger and widgets. |
| `featurebaseJwt` | `string` | **Recommended for logged-in users.** Server-signed JWT carrying identity + custom attributes. See [Section 8](#8-identifying-users-jwt). |
| `messenger` | `boolean` | Advanced. `false` opts out of the messenger even when Support is enabled in the dashboard. Default `true`. |
| `userId`, `email`, `name` | identity | Only needed for non-secured installs, or as non-sensitive supplements when secure-update is disabled per-attribute. Prefer the JWT (see [Section 8](#8-identifying-users-jwt)). |
| `createdAt`, `customData`, `company`, `companies` | profile | Forwarded to the contact. Prefer placing these in the JWT when sensitive. |
| `theme`, `language` | `string` | Initial messenger appearance. |
| `hideDefaultLauncher`, `alignment`, `horizontalPadding`, `verticalPadding` | launcher | Position/visibility of the floating launcher. |
| `jwtToken` | `string` | Pre-minted JWT for widget identity (advanced; usually leave unset). |
| `[key: string]` | `unknown` | Any other key is forwarded verbatim onto the contact profile. |

## Path B — Vanilla (any framework or no framework)

A single `Featurebase()` call. Same shape as the Provider above:

```ts
import Featurebase from 'featurebase-js';

Featurebase({
  appId: 'abc123',                       // Settings → Developers → Installation
  featurebaseJwt: user?.featurebaseJwt,  // Optional — see Section 8
});
```

Subsequent calls with the same `appId` re-identify the visitor without
re-rendering the messenger, so it's safe to call inside any auth-change
handler.

> ✅ **Done.** Anonymous visitors are fully supported — pass `appId`
> alone and skip `featurebaseJwt` entirely. To tie conversations / posts
> / survey responses to a logged-in user, see
> [Section 8 — Identifying users](#8-identifying-users-jwt).

> Jump to [Section 2 (Messenger)](#2-messenger) to control it, or
> [Section 3 / 4 / 5 / 6](#3-changelog-widget) to add the standalone
> widgets — they inherit `appId` and identity from this call.

## What boots when?

The SDK reads your dashboard's **General → Manage modules** toggles to
decide which surfaces to start. You don't repeat the answer in code.

| Module enabled in dashboard? | Effect on `Featurebase({ appId })` |
|---|---|
| Support **on** | Messenger boots automatically. |
| Support **off** | Messenger stays off — nothing is rendered, no console warnings. Flip it on later, no code change needed. |
| Feedback / Changelog / Help Center / Surveys **on** | Available to the matching `init*()` / `useXxxWidget()` call. |
| Module **off** | The runtime refuses to load it and logs a `[featurebase-js]` warning when you call the matching `init*()`. |

> 💡 **Want the messenger off even when Support is enabled?** Pass
> `messenger: false`. Power-user only — most installs leave it unset.

```ts
// Don't boot the messenger here, even if dashboard has Support on.
Featurebase({ appId: 'abc123', messenger: false });
```

## Widgets-only install

Same `Featurebase({ appId })` / `<FeaturebaseProvider appId>` setup as
above. If your workspace has Support disabled in **Manage modules**,
the messenger silently skips boot — there's nothing to do client-side.

## On login / logout

Both paths handle auth changes gracefully:

| | React | Vanilla |
|---|---|---|
| **Login** (user becomes known) | Provider re-renders with new `featurebaseJwt` → auto re-identifies. No re-mount. | Call `Featurebase({ appId, featurebaseJwt, ... })` again — same call as setup. |
| **Logout** (user becomes anonymous) | Conditionally render the Provider, OR call `shutdown()` from `useFeaturebase()`. | Call `shutdown()` (messenger) and/or `clearConfig()` (widgets). |

See [Common patterns](#7-common-patterns) for full code.

---

# 2. Messenger

> ✅ **Already installed** by your setup step above. This section is
> purely about controlling it.

> 👤 **Anonymous works.** The messenger renders fine without identifying
> the user. To attribute conversations to a Featurebase contact, see
> [Section 8 — Identifying users](#8-identifying-users-jwt).

The messenger gives you in-app chat, a help center, conversations, and a
news/changelog feed — all behind one floating launcher. The launcher is
visible by default; opt out with `hideDefaultLauncher: true` and use
your own button.

## Open and close

| Method | What it does |
|---|---|
| `show()` | Open to the default landing space. |
| `hide()` | Close the messenger. |
| `showSpace(space)` | Open to a specific space: `'home' \| 'messages' \| 'help' \| 'changelog'` (or any future space id). |
| `showMessages()` | Shortcut for `showSpace('messages')`. |

```ts
// Vanilla
import { show, hide, showSpace, showMessages } from 'featurebase-js';

show();
hide();
showSpace('help');
showSpace('changelog');
showMessages();
```

```tsx
// React
const { show, hide, showSpace, showMessages } = useFeaturebase();
return <button onClick={show}>Help</button>;
```

## Open specific content

| Method | What it does |
|---|---|
| `showArticle(id)` | Open a specific help-center article by id or slug. |
| `showChangelog(id?)` | Open the changelog index, or a specific entry when `id` is given. |
| `showNews(id)` | Alias for `showChangelog(id)`. |
| `showConversation(id)` | Open a specific conversation thread. |
| `showNewMessage(text?)` | Open the new-message composer, optionally pre-filled. |

```ts
import {
  showArticle,
  showChangelog,
  showConversation,
  showNewMessage,
} from 'featurebase-js';

showArticle('billing-faq');
showChangelog();                              // index
showChangelog('cl_release_4_2_0');            // specific entry
showConversation('conv_8q2x');
showNewMessage();                             // empty composer
showNewMessage('I want to upgrade my plan');  // pre-filled composer
```

## Appearance

| Method | What it does |
|---|---|
| `setTheme('light' \| 'dark')` | Switch the messenger theme at runtime. |
| `setLanguage(code)` | Switch the messenger language (BCP-47 short code). |

```ts
import { setTheme, setLanguage } from 'featurebase-js';

// Sync the messenger with your app's theme toggle
const mql = window.matchMedia('(prefers-color-scheme: dark)');
setTheme(mql.matches ? 'dark' : 'light');
mql.addEventListener('change', (e) => setTheme(e.matches ? 'dark' : 'light'));

setLanguage('de');
setLanguage('pt-BR');
```

See [`FeaturebaseLocale`](#featurebaselocale) for every supported code.

## Events

| Method | Fires when |
|---|---|
| `onShow(cb)` | The messenger becomes visible. |
| `onHide(cb)` | The messenger is dismissed. |
| `onUnreadCountChange(cb)` | The unread-conversation count changes. Receives `(count: number)`. |
| `onExternalLinkOpen(cb)` | The user clicks an outbound link inside the messenger. Receives `(url: string)`. |

> ⚠️ Wrap subscriptions in `whenReady(...)` if you call them in the
> same tick as setup — the messenger boot is async and you'd otherwise
> see "Please call 'boot' first" in the console.

```ts
import {
  whenReady,
  onShow,
  onHide,
  onUnreadCountChange,
  onExternalLinkOpen,
} from 'featurebase-js';

whenReady(() => {
  onShow(() => analytics.track('messenger_opened'));
  onHide(() => analytics.track('messenger_closed'));

  onUnreadCountChange((count) => {
    document.title = count > 0 ? `(${count}) Acme` : 'Acme';
  });

  onExternalLinkOpen((url) => analytics.track('outbound_link', { url }));
});
```

In React, `useFeaturebase()` exposes `unreadCount` directly as reactive
state — no manual subscription needed:

```tsx
const { show, unreadCount } = useFeaturebase();
return <button onClick={show}>Chat ({unreadCount})</button>;
```

## Update identity / shut down

| Method | What it does |
|---|---|
| `update(settings?)` | Re-identify the current visitor without re-booting. Reuses the previously booted `appId` if you don't pass one. |
| `shutdown()` | Tear down the messenger UI, real-time connection, and cached identity. After `shutdown()`, the next `Featurebase()` call fully re-boots. |

```ts
import { update, shutdown } from 'featurebase-js';

// User updates their email in your settings page
update({ email: 'new@email.com' });

// Attach a freshly-onboarded company
update({ company: { companyId: 'co_42', plan: 'enterprise' } });

// Logout
shutdown();
```

In React, the Provider does this automatically when its props change.
You only need to call `shutdown()` if you want to fully tear down before
unmounting.

```tsx
const { update, shutdown } = useFeaturebase();
```

## Reference: every messenger setting

Accepted by `Featurebase()`, `boot()`, `update()`, and the
`<FeaturebaseProvider>` props.

| Field | Type | Default | Notes |
|---|---|---|---|
| `appId` | `string` | — | **Required.** Your Featurebase organization id (the value the dashboard's **Settings → Developers → Installation** copy block exposes). Drives both messenger and widgets. Alias: `app_id`. |
| `messenger` | `boolean` | `true` | Advanced. `false` opts out of the messenger even when **General → Manage modules** has Support enabled. |
| `featurebaseJwt` | `string` | — | **Recommended for logged-in users.** Server-signed JWT carrying identity + custom attributes. See [Section 8](#8-identifying-users-jwt). |
| `userId` | `string \| number` | — | Use only when an attribute is explicitly configured as non-secured in the dashboard. Otherwise put inside the JWT. Alias: `user_id`. |
| `email` | `string` | — | Same as `userId`. |
| `name` | `string` | — | Display name. |
| `createdAt` | `number \| string` | — | Account creation date (Unix ms or ISO 8601). Alias: `created_at`. |
| `company` | `FeaturebaseCompany` | — | Single company association. Prefer the JWT for sensitive fields. |
| `companies` | `FeaturebaseCompany[]` | — | Multiple companies (multi-tenant users). |
| `$brandId` | `string` | — | Active brand id when running multi-brand. |
| `theme` | `'light' \| 'dark'` | `'light'` | Aliases: `themeMode`, `theme_mode`. |
| `language` | `string` | `'en'` | BCP-47 short code. Aliases: `languageOverride`, `language_override`. |
| `hideDefaultLauncher` | `boolean` | `false` | Hide the floating launcher (use your own with `show()`). Alias: `hide_default_launcher`. |
| `alignment` | `'left' \| 'right'` | `'right'` | Launcher side. |
| `horizontalPadding` | `number` | runtime default | px from the side. Alias: `horizontal_padding`. |
| `verticalPadding` | `number` | runtime default | px from the bottom. Alias: `vertical_padding`. |
| `customData` | `Record<string, unknown>` | — | Non-secured custom user attributes. Alias: `custom_data`. |
| `[key: string]` | `unknown` | — | Any other key is forwarded verbatim onto the contact's profile. |

`FeaturebaseCompany` shape:

| Field | Type | Notes |
|---|---|---|
| `companyId` | `string` | Stable company id. Alias: `company_id`. |
| `name`, `plan`, `website`, `industry` | `string` | |
| `createdAt` | `number \| string` | |
| `monthlySpend` | `number` | MRR or similar. |
| `size` | `number` | Headcount. |
| `[key: string]` | `unknown` | Catchall — forwarded to the company profile. |

Both camelCase (preferred) and snake_case keys are accepted; the wrapper
normalizes to camelCase before dispatching.

> 🔒 **Sensitive vs non-sensitive attributes.** Anything that should be
> trusted (display name, plan, role, etc.) belongs **inside the JWT**.
> Plain-text props like `userId` / `email` outside the JWT are only safe
> when the corresponding attribute is explicitly marked non-secured in
> your dashboard settings. See the [security guide](https://help.featurebase.app/en/help/articles/5402549-secure-your-installation-required-by-default).

---

# 3. Changelog widget

> 👤 **Anonymous works.** Renders the latest entries fine without
> identifying the user. To pre-personalize the popup greeting and track
> per-user read state, see
> [Section 8 — Identifying users](#8-identifying-users-jwt).

A standalone "What's new" surface, independent from the messenger. Three
sub-modes you can mix and match:

- **dropdown** — attaches to a `<button data-featurebase-changelog>` in
  your nav and renders a dropdown panel.
- **popup** — auto-displays a fullscreen popup of the latest unread
  update.
- **changelogCard** — floating bottom-corner card.

## Standalone install — no messenger

If your workspace has Support disabled (or you set `messenger: false`),
[Section 1](#1-setup) already gave you the only setup call you need.
The snippets below are complete copy-paste samples for the changelog
widget alone.

### Vanilla

```ts
import Featurebase, { initChangelog } from 'featurebase-js';

// 1) One-time setup. Same call as Section 1 — appId drives the slug.
Featurebase({
  appId: 'abc123',
  featurebaseJwt: user?.featurebaseJwt, // optional, see Section 8
});

// 2) Boot the widget. Re-call to change config; `destroyChangelog()` to remove.
//    If 1) hasn't finished resolving yet, this call is queued and fires
//    automatically once the slug lands.
initChangelog({
  theme: 'light',
  dropdown: { enabled: true, placement: 'bottom' },
});
```

```html
<button data-featurebase-changelog>What's new</button>
```

### React

```tsx
import { FeaturebaseProvider, useChangelogWidget } from 'featurebase-js/react';

function App({ user }) {
  return (
    <FeaturebaseProvider appId="abc123" featurebaseJwt={user?.featurebaseJwt}>
      <Header />
    </FeaturebaseProvider>
  );
}

function Header() {
  const { getUnviewedCount } = useChangelogWidget({
    theme: 'light',
    dropdown: { enabled: true, placement: 'bottom' },
  });
  return (
    <button data-featurebase-changelog>
      What's new ({getUnviewedCount() ?? 0})
    </button>
  );
}
```

The hook boots the widget on mount, tears it down on unmount, and
re-inits only when the resolved config meaningfully changes (so passing
inline object literals doesn't thrash).

## Already set up?

Identity (`appId`, `featurebaseJwt`) is inherited from
[Setup](#1-setup) — `<FeaturebaseProvider>` in React or your
`Featurebase()` call in vanilla. Drop straight into:

```tsx
// React
useChangelogWidget({ theme: 'light', dropdown: { enabled: true } });
```

```ts
// Vanilla
initChangelog({ theme: 'light', dropdown: { enabled: true } });
```

## Sub-modes

### Dropdown

```ts
initChangelog({
  theme: 'light',
  dropdown: {
    enabled: true,
    placement: 'bottom',           // 'bottom' | 'top' | 'left' | 'right' | 'auto'
    lazyLoad: true,                // defer fetching until first open
  },
});
```

Bind to any element matching `button[data-featurebase-changelog]`:

```html
<button data-featurebase-changelog>What's new</button>
```

### Popup

Auto-displays a fullscreen popup of the latest unread update.

```ts
initChangelog({
  theme: 'dark',
  popup: {
    enabled: true,
    autoOpenForNewUpdates: true,
    usersName: user.firstName,     // personalizes the greeting
  },
});
```

### Floating card

A small announcement card pinned to a corner.

```ts
initChangelog({
  theme: 'light',
  changelogCard: {
    enabled: true,
    openInNewTab: true,
    layout: { position: 'bottom-right', marginBottom: 24, maxWidth: 360 },
    theme: { borderRadius: 12, backgroundColor: '#ffffff', titleColor: '#111827' },
  },
});
```

### All three together

```ts
initChangelog({
  theme: 'light',
  dropdown: { enabled: true },
  popup: { enabled: true, autoOpenForNewUpdates: true },
  changelogCard: { enabled: true, layout: { position: 'bottom-right' } },
});
```

## Imperative methods

Available from any tick after `initChangelog` has been called.

| Method | Returns | Notes |
|---|---|---|
| `openChangelogPopup(data?)` | `void` | Open the popup (requires `popup.enabled` in init). |
| `markAllChangelogsAsViewed()` | `void` | Mark every entry as read for the current visitor. |
| `getUnviewedChangelogCount()` | `number \| undefined` | Synchronous read of the unread count. `undefined` before init or in SSR. |
| `destroyChangelog()` | `void` | Tear down DOM, listeners, and cached state. |

```ts
import {
  openChangelogPopup,
  markAllChangelogsAsViewed,
  getUnviewedChangelogCount,
  destroyChangelog,
} from 'featurebase-js';

const unread = getUnviewedChangelogCount() ?? 0;
document.querySelector('#whatsnew-badge').textContent = String(unread);

openChangelogPopup();
markAllChangelogsAsViewed();
destroyChangelog();
```

In React these are returned from `useChangelogWidget()`:

```tsx
const { openPopup, markAllAsViewed, getUnviewedCount } = useChangelogWidget({ ... });
```

## Reference: `ChangelogWidgetConfig`

| Field | Type | Required | Notes |
|---|---|---|---|
| `theme` | `'light' \| 'dark'` | ✓ | Widget shell theme. |
| `dropdown` | `{ enabled, placement?, lazyLoad? }` | one of dropdown/popup/changelogCard | See above. |
| `popup` | `{ enabled, usersName?, autoOpenForNewUpdates? }` | "" | See above. |
| `changelogCard` | `{ enabled, openInNewTab?, layout?, theme? }` | "" | See sub-table below. |
| `category` | `string[]` | — | Filter to a subset of category slugs. |
| `locale` | `FeaturebaseLocale` | — | UI language. Defaults to `'en'`. |
| `featurebaseJwt` | `string` | — | Server-signed JWT. Inherited from setup; pass to override. |
| `email`, `userId` | identity | — | Only when configured as non-secured in dashboard. Prefer the JWT. |

`changelogCard` sub-fields:

| Field | Type | Notes |
|---|---|---|
| `enabled` | `boolean` | |
| `openInNewTab` | `boolean` | |
| `layout.position` | `'bottom-left' \| 'bottom-right'` | |
| `layout.marginBottom` | `number` | px from the bottom. |
| `layout.marginSide` | `number` | px from the side. |
| `layout.maxWidth` | `number` | px max-width of the card. |
| `theme.borderRadius` | `number` | |
| `theme.backgroundColor`, `theme.titleColor`, `theme.descriptionColor`, `theme.borderColor` | `string` | Any CSS color value. |

> At least one of `dropdown`, `popup`, or `changelogCard` must have
> `enabled: true` — otherwise the widget has nothing to render.

---

# 4. Feedback widget

> 👤 **Anonymous works.** Visitors can browse boards and submit posts
> without being logged in. To submit posts as the signed-in user (no
> email prompt) and tie them to a Featurebase contact, see
> [Section 8 — Identifying users](#8-identifying-users-jwt).

A floating button (bottom-left or bottom-right) that opens a panel where
users browse your Featurebase boards and submit posts without leaving
your app.

## Standalone install — no messenger

```ts
import Featurebase, { initFeedback, destroyFeedback } from 'featurebase-js';

Featurebase({
  appId: 'abc123',
  featurebaseJwt: user?.featurebaseJwt,    // optional
});

initFeedback({
  theme: 'light',
  placement: 'bottom-right',
  defaultBoard: 'feature-requests',
  metadata: { plan: 'pro', appVersion: '4.2.0' },
});

// Later (e.g. when navigating away in an SPA)
destroyFeedback();
```

```tsx
// React equivalent
import { FeaturebaseProvider, useFeedbackWidget } from 'featurebase-js/react';

function App({ user }) {
  return (
    <FeaturebaseProvider appId="abc123" featurebaseJwt={user?.featurebaseJwt}>
      <FeedbackHost />
    </FeaturebaseProvider>
  );
}

function FeedbackHost() {
  useFeedbackWidget({
    theme: 'light',
    placement: 'bottom-right',
    defaultBoard: 'feature-requests',
  });
  return null;
}
```

The hook boots the widget on mount and tears it down on unmount.

## Already set up?

Identity inherited from [Setup](#1-setup). Just drop:

```tsx
useFeedbackWidget({ theme: 'light', placement: 'bottom-right' });
```

```ts
initFeedback({ theme: 'light', placement: 'bottom-right' });
```

## Reference: `FeedbackWidgetConfig`

| Field | Type | Required | Notes |
|---|---|---|---|
| `theme` | `'light' \| 'dark'` | ✓ | Widget shell theme. |
| `placement` | `'bottom-right' \| 'bottom-left' \| 'left' \| 'right'` | — | Floating button position. Defaults to `'bottom-right'`. |
| `defaultBoard` | `string` | — | Slug of the board to open the panel on. |
| `metadata` | `Record<string, string> \| null` | — | Arbitrary string attributes attached to every post submitted while the widget is mounted (e.g. plan, version). |
| `locale` | `FeaturebaseLocale` | — | UI language. |
| `featurebaseJwt` | `string` | — | Server-signed JWT. Inherited from setup; pass to override. |
| `email`, `userId` | identity | — | Only when configured as non-secured in dashboard. Prefer the JWT. |

---

# 5. Surveys

> 👤 **Anonymous works** for any survey whose dashboard targeting
> doesn't reference user attributes. To target by plan / role / segment
> or tie responses to a Featurebase contact, see
> [Section 8 — Identifying users](#8-identifying-users-jwt).

Background runtime that polls for active surveys whose targeting rules
match the current page (URL pattern, CSS selector, segment) and
auto-displays them. **Targeting is owned by the dashboard** — there's no
imperative `showSurvey(id)`.

## Standalone install — no messenger

```ts
import Featurebase, { initSurveys } from 'featurebase-js';

Featurebase({
  appId: 'abc123',
  featurebaseJwt: user?.featurebaseJwt,  // required for user-attribute targeting; see Section 8
});

initSurveys();                 // mount once and forget — no destroy needed
```

```tsx
// React equivalent
import { FeaturebaseProvider, useSurveyWidget } from 'featurebase-js/react';

function App({ user }) {
  return (
    <FeaturebaseProvider appId="abc123" featurebaseJwt={user?.featurebaseJwt}>
      <SurveyHost />
    </FeaturebaseProvider>
  );
}

function SurveyHost() {
  useSurveyWidget();
  return null;
}
```

## Already set up?

Identity inherited from [Setup](#1-setup). Mount once at the app root:

```tsx
useSurveyWidget();
```

```ts
initSurveys();
```

You can still pass overrides per call when you need them:

```ts
initSurveys({ placement: 'bottom-right', featurebaseJwt: otherJwt });
```

## Runtime behavior

- Fetches all active surveys for the org once on init, then **polls every
  5 seconds** to see whether any targeting rule matches the current page.
- Once a user **responds or dismisses** a survey, its id is stored in
  `localStorage.featurebaseRespondedSurveys` and won't show again on the
  same browser.
- `initSurveys` is idempotent — calling it again while a survey is
  already mounted is a no-op.
- There is **no `destroySurveys`** in the runtime today; mount it once.

## Reference: `SurveyWidgetConfig`

| Field | Type | Required | Notes |
|---|---|---|---|
| `placement` | `'bottom-left' \| 'bottom-right' \| null` | — | Where the panel anchors. `null` defers to the dashboard. |
| `theme` | `string` | — | Override the survey's configured theme. |
| `locale` | `FeaturebaseLocale` | — | UI language. |
| `featurebaseJwt` | `string` | — | Server-signed JWT. Required when targeting includes "logged-in users only" or segment rules that depend on the user's profile. Inherited from setup. |
| `email`, `userId` | identity | — | Only when configured as non-secured in dashboard. Prefer the JWT. |

---

# 6. Embed widget

> 👤 **Anonymous works.** Visitors can browse the embedded portal, vote,
> and submit posts without being logged in. To attribute their actions to
> a Featurebase contact (and skip the email prompt on submit), see
> [Section 8 — Identifying users](#8-identifying-users-jwt).

Mounts a Featurebase board, roadmap, or changelog page **inside your own
app** as an iframe — no popup, no floating launcher. Use it to ship a
fully-branded `/feedback`, `/roadmap`, or `/changelog` route that lives
under your domain instead of redirecting users to a Featurebase-hosted
portal.

The runtime mounts into any element with the `data-featurebase-embed`
attribute; you control its size, layout, and surrounding chrome. The
embed bundle itself is **lazy-loaded** — only customers who actually call
`initEmbedWidget()` / `useEmbedWidget()` download the iframe-resizer
chunk. Messenger-only and widget-only customers pay nothing for it.

## Standalone install — no messenger

```ts
import Featurebase, { initEmbedWidget } from 'featurebase-js';

Featurebase({
  appId: 'abc123',
  featurebaseJwt: user?.featurebaseJwt,    // optional
});

initEmbedWidget(
  {
    embedOptions: {
      // '/' (boards), '/roadmap', '/changelog', '/p/<post-slug>'
      path: '/',
      // Optional query-string filters (without the leading '?').
      // Example: pre-filter the boards index to one board, sorted by upvotes.
      filters: '',  // e.g. 'b=63f827df2d62cb301468aac4&sortBy=upvotes:desc'
      // ADVANCED — mirror iframe routes onto the host URL for deep-linking.
      // routeSyncingBasePath: '/feedback',
    },
    stylingOptions: {
      // 'light' | 'dark' | '' (auto-detect from prefers-color-scheme)
      theme: 'light',
      hideMenu: false,   // hide the top nav (Boards / Roadmap / Changelog)
      hideLogo: false,   // hide the Featurebase logo in the nav bar
    },
    // Identity is auto-forwarded from the universal Featurebase({ appId,
    // featurebaseJwt }) call above. Optional: attach arbitrary metadata
    // to posts / votes submitted from inside the iframe.
    // user: { metadata: { plan: 'pro', appVersion: '4.2.0' } },
  },
  // Optional callback — fires on lifecycle events (`loaded`, `widgetReady`)
  // and on every route change inside the iframe.
  (error, evt) => {
    if (error) return console.error('[embed]', error);
    if (evt?.action === 'widgetReady') console.log('embed ready');
  },
);
```

```html
<!-- Anywhere in the page; the SDK mounts here. -->
<div data-featurebase-embed style="height: 100vh"></div>
```

```tsx
// React equivalent
import { FeaturebaseProvider, useEmbedWidget } from 'featurebase-js/react';

function App({ user }) {
  return (
    <FeaturebaseProvider appId="abc123" featurebaseJwt={user?.featurebaseJwt}>
      <FeedbackPage />
    </FeaturebaseProvider>
  );
}

function FeedbackPage() {
  useEmbedWidget({
    embedOptions: {
      path: '/',                            // '/' | '/roadmap' | '/changelog' | '/p/<post>'
      // filters: 'b=63f827df2d62cb301468aac4&sortBy=upvotes:desc',
      // routeSyncingBasePath: '/feedback', // ADVANCED — see below
    },
    stylingOptions: {
      theme: 'light',                       // 'light' | 'dark' | '' (auto)
      hideMenu: false,
      hideLogo: false,
    },
  });
  return <div data-featurebase-embed style={{ height: '100vh' }} />;
}
```

The hook re-inits whenever the resolved config changes — the runtime
treats subsequent calls as **in-place updates** of the existing iframe,
so flipping themes or paths doesn't remount the board (no flash, no
scroll-to-top). The iframe itself is owned by the
`data-featurebase-embed` element you put in your tree, so unmounting
the host element tears it down via the React lifecycle — no separate
`destroy*()` call is needed.

## Already set up?

Identity inherited from [Setup](#1-setup). Drop straight into:

```tsx
useEmbedWidget({
  stylingOptions: { theme: 'light' },
  embedOptions: { path: '/roadmap' },
});
```

```ts
initEmbedWidget({
  stylingOptions: { theme: 'light' },
  embedOptions: { path: '/roadmap' },
});
```

## Common recipes

### Embedded roadmap page

```tsx
useEmbedWidget({
  stylingOptions: { theme: 'light', hideMenu: true, hideLogo: true },
  embedOptions: { path: '/roadmap' },
});
```

### Embedded board with URL syncing

Reflect navigation inside the iframe (e.g. opening a post) onto your
host URL so users can deep-link / back-button:

```tsx
useEmbedWidget({
  stylingOptions: { theme: 'light' },
  embedOptions: {
    path: '/',
    routeSyncingBasePath: '/feedback',  // host URL becomes /feedback/p/abc123
  },
});
```

### Following the host app theme

```tsx
const { theme } = useTheme();             // your own theme hook
useEmbedWidget({
  stylingOptions: { theme },              // 'light' | 'dark'
  embedOptions: { path: '/' },
});
```

## Reference: `FeaturebaseEmbedWidgetConfig`

| Field | Type | Required | Notes |
|---|---|---|---|
| `embedOptions.path` | `string \| null` | — | Path to mount inside the portal. Common values: `'/'` (feedback boards index — default), `'/roadmap'`, `'/changelog'`, `'/p/<post-slug>'`. |
| `embedOptions.filters` | `string \| null` | — | Query-string filters applied to the mounted page (without the leading `?`). Example: `'b=63f827df2d62cb301468aac4&sortBy=upvotes:desc'` pre-filters the boards index to one board sorted by upvotes. |
| `embedOptions.routeSyncingBasePath` | `string \| null` | — | **Advanced.** Reflect iframe route changes onto the host URL under this prefix. Example: `'/feedback'` → host URL becomes `/feedback/p/abc123` when the visitor opens a post. Defaults to no syncing. |
| `stylingOptions.theme` | `'light' \| 'dark' \| ''` | — | Force a theme inside the iframe. `''` auto-detects from `prefers-color-scheme`. Omitted / `null` falls back to the workspace default theme. |
| `stylingOptions.hideMenu` | `boolean` | — | Hide the top navigation bar inside the iframe (Boards / Roadmap / Changelog tabs). Useful when embedding a single surface and the host page has its own navigation. Defaults to `false`. |
| `stylingOptions.hideLogo` | `boolean` | — | Hide the Featurebase logo in the navigation bar. Defaults to `false`. |
| `user.metadata` | `Record<string, string>` | — | Arbitrary string attributes attached to posts / votes submitted from inside the iframe (e.g. `{ plan: 'pro', appVersion: '4.2.0' }`). Not user identity — see below. |

> 🔑 **Identity & workspace are wrapper-managed.** You never pass
> `appId` / `organization` / `userId` / `email` / `featurebaseJwt` per
> embed call. They flow in once through the
> `Featurebase({ appId, featurebaseJwt })` call (or `<FeaturebaseProvider>`
> in [Setup](#1-setup)) and the wrapper auto-forwards them to every
> mount. To re-identify, update the universal install — not the embed
> config.

### Callback events

`initEmbedWidget(config, callback)` and `useEmbedWidget(config, callback)`
both accept an optional callback that fires for runtime lifecycle events
and route changes from inside the iframe:

| `evt.action` | When |
|---|---|
| `'loaded'` | Iframe document has loaded but the embed UI is still booting. |
| `'widgetReady'` | Embed UI is fully booted and interactive. |
| `'routeChange'` | User navigated inside the iframe (carries `path` / `url`). |

Future actions are surfaced verbatim — the runtime forwards anything new
without an SDK upgrade, so it's safe to switch on `evt.action` with a
default branch.

```ts
initEmbedWidget(config, (err, evt) => {
  if (err) return console.error('[embed]', err);
  switch (evt?.action) {
    case 'widgetReady':
      analytics.track('embed_ready');
      break;
    case 'routeChange':
      analytics.track('embed_navigate', { path: evt.path });
      break;
  }
});
```

---

# 7. Common patterns

## Login

### React — automatic

The Provider re-identifies on its own. Just keep its props in sync with
your auth state.

```tsx
function Root() {
  const { user } = useAuth();
  return (
    <FeaturebaseProvider
      appId="org_xyz"
      featurebaseJwt={user?.featurebaseJwt}
    >
      <App />
    </FeaturebaseProvider>
  );
}
```

When `user` becomes truthy, the SDK re-identifies the visitor without
re-rendering the messenger, and widget hooks pick up the new identity
automatically.

### Vanilla

Just re-call `Featurebase()` with the new identity. Idempotent —
re-runs `identify` for the messenger and refreshes widget defaults in
the same call. Same one-field shape as setup:

```ts
import Featurebase from 'featurebase-js';

async function onLogin(user) {
  Featurebase({
    appId: 'abc123',
    featurebaseJwt: user.featurebaseJwt,
  });
}
```

## Logout

```tsx
// React — either conditionally render the Provider…
{user ? (
  <FeaturebaseProvider {...identity}><App /></FeaturebaseProvider>
) : (
  <App />
)}

// …or call shutdown explicitly from useFeaturebase
const { shutdown } = useFeaturebase();
function onLogout() {
  shutdown();
}
```

```ts
// Vanilla — tear down whatever you booted
import { shutdown, clearConfig } from 'featurebase-js';

function onLogout() {
  shutdown();         // messenger (skip if you didn't boot one)
  clearConfig();      // widget identity defaults
}
```

## Themed messenger that follows the app

```ts
import { setTheme } from 'featurebase-js';

const mql = window.matchMedia('(prefers-color-scheme: dark)');
setTheme(mql.matches ? 'dark' : 'light');
mql.addEventListener('change', (e) => setTheme(e.matches ? 'dark' : 'light'));
```

## Per-route widgets in an SPA

```ts
// React — automatic, useFeedbackWidget tears down on unmount
{onFeedbackPage && <FeedbackHost />}

// Vanilla
router.beforeEach((to, from, next) => {
  if (from.path.startsWith('/app')) destroyFeedback();
  if (to.path.startsWith('/app'))
    initFeedback({ theme: 'light', placement: 'bottom-right' });
  next();
});
```

---

# 8. Identifying users (JWT)

> Skip this section if you only need anonymous use. Every surface in
> Sections 2–6 render fine without it.

Identifying a user lets Featurebase tie messenger conversations,
feedback posts, changelog reads, and survey responses to a real contact
in your dashboard — and unlocks user-attribute targeting (e.g. only show
this survey to `plan: 'pro'` users). Featurebase secures this with
**JWTs signed on your server**: without a valid JWT, the runtime refuses
to identify the user and you'll see a console warning.

> 📚 Full guide: [Secure your installation](https://help.featurebase.app/en/help/articles/5402549-secure-your-installation-required-by-default)
> · [Creating and signing a JWT](https://help.featurebase.app/en/articles/5257986-creating-and-signing-a-jwt)

## 8.1. Get your JWT secret

Dashboard → **Settings → Access & Security → Security**. Copy the
private key into a server-side env var. **Never expose it to the
browser.**

```bash
FEATUREBASE_JWT_SECRET=…
```

## 8.2. Generate the JWT on your server

Sign every authenticated user's profile data with HS256. Both `email`
and `userId` should be included when you have them; one of the two is
required.

```ts
// Node / your backend
import jwt from 'jsonwebtoken';

function generateFeaturebaseJwt(user) {
  return jwt.sign(
    {
      // Identity (one of email/userId is required)
      userId: user.id,
      email: user.email,
      name: user.name,
      profilePicture: user.avatarUrl,

      // Optional custom attributes — must be configured in dashboard first
      plan: user.plan,
      title: user.jobTitle,

      // Optional company associations
      companies: [
        {
          id: user.companyId,           // required
          name: user.companyName,        // required
          monthlySpend: 500,             // optional
          createdAt: '2023-05-19T15:35:49.915Z',
        },
      ],
    },
    process.env.FEATUREBASE_JWT_SECRET,
    { algorithm: 'HS256' },
  );
}
```

## 8.3. Pass it once at setup

Send the resulting string to the browser via your normal API / SSR
props, then pass it as `featurebaseJwt` once in your [Section 1 setup
call](#1-setup). The SDK auto-propagates it to every surface (messenger
+ widgets):

```ts
// React: identity flows through one Provider tag
<FeaturebaseProvider featurebaseJwt={user.featurebaseJwt} {...rest}>...</FeaturebaseProvider>

// Vanilla: identity flows through one Featurebase() call
Featurebase({ featurebaseJwt: user.featurebaseJwt, ...rest });
```

The JWT carries `userId`, `email`, `name`, custom attributes, and
companies, so you don't need to pass them as separate props (and
shouldn't — anything you'd want secured belongs inside the JWT).

## 8.4. Validate it works

Dashboard → **Settings → Access & Security → Security → Validate JWT**.
Paste a freshly-generated token to confirm everything is signed
correctly before going to production.

> ⚠️ For local testing only, you can disable secure login in the
> dashboard. **Never** ship to production with it disabled — anyone
> could impersonate any user.

For login / logout flows that re-identify or tear down the visitor, see
[Section 7 — Common patterns](#7-common-patterns).

---

# React reference

## `<FeaturebaseProvider>`

See [Setup → Path A](#path-a--react) for the full props table.

What it does on mount/unmount/re-render:

- **Mount** (with `appId`): resolves `appId → { slug, modules }` (cached
  aggressively), boots the messenger when **General → Manage modules**
  has Support enabled (or `messenger` prop is `true`), subscribes to the
  unread-count stream, and exposes identity to descendants via context.
- **Re-render with new identity props**: dispatches `identify`. Messenger
  iframe stays mounted; auth token refreshes.
- **Unmount**: dispatches `shutdown()`.
- **Errors**: any throw from the runtime is caught and logged with
  `[featurebase-js]` prefix. It never bubbles into your React tree.

## `useFeaturebase()`

Imperative + reactive access to the messenger. Methods are stable
references; only `unreadCount` triggers re-renders.

```ts
const {
  show, hide, showSpace, showMessages,
  showArticle, showChangelog, showNews, showConversation, showNewMessage,
  setTheme, setLanguage,
  update, shutdown,
  unreadCount,
} = useFeaturebase();
```

## Widget hooks

| Hook | Returns | Tears down on unmount? |
|---|---|---|
| `useChangelogWidget(config)` | `{ openPopup, markAllAsViewed, getUnviewedCount }` | Yes |
| `useFeedbackWidget(config)` | `void` | Yes |
| `useSurveyWidget(config?)` | `void` | No (runtime has no destroy) |
| `useEmbedWidget(config, callback?)` | `void` | Yes — via the host element's lifecycle |

All three:

- Inherit the resolved org slug and identity (`featurebaseJwt`,
  `jwtToken`, plus non-secured `userId` / `email` when explicitly passed)
  from `<FeaturebaseProvider>` context.
- Re-init only when the **resolved** config (after merge with context)
  meaningfully changes — passing inline object literals doesn't thrash.
- Tolerate the brief tick where the resolver hasn't returned the slug
  yet: the underlying `init*()` queues itself and fires once the slug
  lands.
- Wrap the runtime call in try/catch — any runtime error (including a
  truly unresolvable slug) is logged with `[featurebase-js]` prefix and
  never crashes the React tree.

---

# Vanilla reference

## `Featurebase(settings)` — unified entry point

Default export. Pass `appId` and the SDK does the right thing:

| `settings` contains | Behavior |
|---|---|
| `appId` only | Resolves `appId → { slug, modules }`, sets widget defaults from the slug, and boots the messenger when `modules.support === true`. |
| `appId, messenger: false` | Same as above, but skips the messenger boot. |
| Missing `appId` | Throws — `appId` is required. |

Safe to call inside React effects or any re-running lifecycle hook.
`featurebaseJwt` (and other identity fields) sync to both surfaces in
one shot.

## `boot(settings)` — strict messenger-only

Lower-level than `Featurebase()`. Requires `appId`, only boots the
messenger, **never** mutates the global widget-defaults state, and
**never** consults the module-flag autoboot logic. Use this when you
want to bypass the resolver entirely and force a messenger boot.

## `resolveOrganization(appId)` — sync helper (advanced)

Returns a `Promise<OrganizationInfo>` with the org's `slug`,
`displayName`, and per-product `modules` flags. Cached in-memory and via
`localStorage` (1h TTL). Most callers don't need to invoke this directly
— `Featurebase()` does it for you. Useful when you want to inspect
`modules` before mounting your own UI.

## `update(settings?)` — re-identify without rebooting

Dispatches `identify` directly. Reuses the previously booted `appId` if
omitted.

## `shutdown()` — tear everything down

Removes the messenger DOM/iframe, kills the websocket, drops cached
identity. Next `Featurebase({ appId })` call fully reboots.

## `whenReady(cb)` — defer until boot finishes

The runtime's boot is async. Subscribe to events from inside `whenReady`
to avoid the "Please call 'boot' first" warning.

## `configure(defaults)` — widget defaults (advanced)

Lower-level than `Featurebase()`. Sets widget identity defaults
directly. Most callers don't need this — `Featurebase({ appId })`
already syncs identity defaults and resolves the workspace slug. Use
`configure()` only when you want to refresh widget identity without
re-running the appId resolver.

```ts
import Featurebase, { configure, clearConfig, getWidgetDefaults } from 'featurebase-js';

Featurebase({ appId: 'abc123', messenger: false });
configure({ featurebaseJwt: 'jwt_v1' });
configure({ featurebaseJwt: 'jwt_v2' });        // merges (last-write-wins per key)
getWidgetDefaults();                             // { featurebaseJwt: 'jwt_v2' }
clearConfig();                                   // wipes identity + resolved workspace slug (use on logout)
```

`WidgetDefaults` shape:

| Field | Type | Notes |
|---|---|---|
| `featurebaseJwt` | `string` | **Recommended.** Server-signed JWT carrying identity. |
| `jwtToken` | `string` | Pre-minted JWT (advanced). |
| `userId`, `email` | `string` | Only when configured as non-secured in dashboard. Prefer the JWT. |
| `locale` | `FeaturebaseLocale` | Default UI locale for every widget. |

Behavior:

- **Explicit fields on the call always win** over `configure()` defaults.
- **Resolver-aware** — `init*()` calls made before `Featurebase({ appId })`'s
  resolver settles are queued and dispatched once the slug lands. If no
  `Featurebase({ appId })` call ever initializes the workspace, the call
  throws on the next attempt with a clear message naming the caller.
- **SSR-safe** — every function is a no-op when `window` is undefined.

---

# TypeScript

Every config is a typed interface; importing them is encouraged for
shared component props:

```ts
import type {
  // Messenger
  FeaturebaseSettings,
  FeaturebaseCompany,
  ThemeMode,
  Space,
  // Widgets
  ChangelogWidgetConfig,
  ChangelogWidgetConfigInput,    // workspace slug is resolved from appId
  FeedbackWidgetConfig,
  FeedbackWidgetConfigInput,
  SurveyWidgetConfig,
  SurveyWidgetConfigInput,
  FeaturebaseEmbedWidgetConfig,
  FeaturebaseEmbedWidgetConfigInput,
  FeaturebaseEmbedAction,
  FeaturebaseEmbedCallback,
  // Shared
  FeaturebaseLocale,
  FeaturebaseWidgetIdentity,
  WidgetDefaults,
} from 'featurebase-js';
```

Forward-compat: every config has an index signature
(`[key: string]: unknown`) so future runtime fields are accepted without
needing an SDK upgrade.

## `FeaturebaseLocale`

BCP-47 short codes accepted by every surface. Unknown codes are
forwarded to the runtime verbatim.

```
bn  bs  pt-BR  bg  ca  hr  cs  da  nl  en  et  fi  fr  de  el  hi
hu  id  it  ja  ko  lv  lt  ms  mn  nb  pl  pt  ro  ru  sr  zh-CN
sl  es  sw  sv  th  zh-TW  tr  uk  vi  sk
```

---

# SSR / Next.js

Every export is a no-op when `window` is undefined, so server-side
imports are safe:

```tsx
// app/page.tsx — Server Component, runs on the server
import Featurebase from 'featurebase-js';

Featurebase({ appId: 'org_xyz' });   // safe — silently no-ops
```

For real client-side boot, use the React Provider in a `'use client'`
boundary (recommended) or call `Featurebase()` from a `useEffect`.

```tsx
'use client';
import { FeaturebaseProvider } from 'featurebase-js/react';

export function FeaturebaseRoot({ children }) {
  return (
    <FeaturebaseProvider appId="org_xyz">{children}</FeaturebaseProvider>
  );
}
```

---

# Browser support

Modern evergreen browsers (Chrome / Edge / Firefox / Safari, last 2
versions). The bundle ships ES2018; no polyfills required.

# Versioning

The SDK follows [semantic versioning](https://semver.org/) from `1.0.0`
onward: breaking changes only land in major bumps (`2.0.0`, `3.0.0`, …),
new features in minor bumps (`1.x.0`), and fixes in patch bumps
(`1.0.x`). Pre-release builds publish under the `next` and `beta`
dist-tags so production installs of `latest` stay stable until a release
is promoted.

# Source / contributing

The wrapper is the public surface of the
[Featurebase runtime](https://github.com/FeaturebaseHQ/sdk). To file a
bug or request, open a ticket from the in-app chat at
[featurebase.app](https://featurebase.app) — that lands directly in our
dashboard.

# License

MIT © Featurebase
