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-jsPeer 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 —
appIdalone 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
appIdalone and skipfeaturebaseJwtentirely. 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
appIdand 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 composerAppearance
| 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/
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, orchangelogCardmust haveenabled: 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.featurebaseRespondedSurveysand won't show again on the same browser. initSurveysis idempotent — calling it again while a survey is already mounted is a no-op.- There is no
destroySurveysin 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/featurebaseJwtper embed call. They flow in once through theFeaturebase({ 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): resolvesappId → { slug, modules }(cached aggressively), boots the messenger when General → Manage modules has Support enabled (ormessengerprop istrue), 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-secureduserId/emailwhen 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-aware —
init*()calls made beforeFeaturebase({ appId })'s resolver settles are queued and dispatched once the slug lands. If noFeaturebase({ 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
windowis 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 skSSR / 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-opsFor 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
