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

featurebase-js

v1.0.3

Published

Typed client SDK for Featurebase — messenger, changelog widget, feedback widget, and surveys.

Readme

featurebase-js

Typed JavaScript / TypeScript SDK for Featurebase. 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

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.

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

Jump to Section 2 (Messenger) to control it, or Section 3 / 4 / 5 / 6 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. | | 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). | | 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:

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.

Jump to Section 2 (Messenger) to control it, or Section 3 / 4 / 5 / 6 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.

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

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

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

show();
hide();
showSpace('help');
showSpace('changelog');
showMessages();
// 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. |

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

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

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:

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

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

// User updates their email in your settings page
update({ email: '[email protected]' });

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

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


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.

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 already gave you the only setup call you need. The snippets below are complete copy-paste samples for the changelog widget alone.

Vanilla

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' },
});
<button data-featurebase-changelog>What's new</button>

React

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<FeaturebaseProvider> in React or your Featurebase() call in vanilla. Drop straight into:

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

Sub-modes

Dropdown

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

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

Popup

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

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

Floating card

A small announcement card pinned to a corner.

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

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

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():

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.

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

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();
// 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. Just drop:

useFeedbackWidget({ theme: 'light', placement: 'bottom-right' });
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.

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

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
// 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. Mount once at the app root:

useSurveyWidget();
initSurveys();

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

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.

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

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');
  },
);
<!-- Anywhere in the page; the SDK mounts here. -->
<div data-featurebase-embed style="height: 100vh"></div>
// 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. Drop straight into:

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

Common recipes

Embedded roadmap page

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:

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

Following the host app theme

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

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.

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:

import Featurebase from 'featurebase-js';

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

Logout

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

// …or call shutdown explicitly from useFeaturebase
const { shutdown } = useFeaturebase();
function onLogout() {
  shutdown();
}
// 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

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

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

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.

// 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. The SDK auto-propagates it to every surface (messenger

  • widgets):
// 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.


React reference

<FeaturebaseProvider>

See Setup → Path A 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.

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.

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-awareinit*() 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:

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:

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

'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 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. To file a bug or request, open a ticket from the in-app chat at featurebase.app — that lands directly in our dashboard.

License

MIT © Featurebase