Dokumentation

Widget

Drop a floating feedback launcher and dialog into any web app with a single script tag — no iframe wrapper to build, no page to spin up.

The widget is a one-line install. Paste a <script> tag into your site and you get a floating Feedback pill in the corner that opens a feedback panel — submit ideas, browse posts, vote, comment. The same panel can also be opened from your own UI (a menu item, a help link) without the floating button.

This is the fastest way to put Boarbs in front of your users. If you'd rather embed the full board as a dedicated page inside your app, see Embed instead.

How the Widget Compares

WidgetEmbed (iframe)Portal
InstallOne <script> tagPage + iframe + backend sessionZero — hosted at your subdomain
Where it livesEvery page of your appA dedicated page inside your appA separate domain (yourco.boarbs.com)
AuthAnonymous or session tokenAlways session tokenAnonymous or portal sign-in
Best forAlways-on feedback, support, NPSA "Feedback" tab inside your appPublic roadmap, no-code launch

Quick Install (5 Minutes)

Paste before </body> on every page where you want feedback collection:

<script async src="https://YOUR_BOARBS_DOMAIN/widget/v1/widget.js"></script>
<script>
  window.Boarbs = window.Boarbs || (function () {
    var q = []; var f = function () { q.push(arguments); };
    f.q = q; f._stub = true; return f;
  })();

  window.Boarbs.init({
    workspace: "YOUR_WORKSPACE_SLUG", // optional if the script already loads from your workspace domain
    mode: "launcher",                 // "launcher" | "trigger"
    position: "bottom-right",         // "bottom-right" | "bottom-left"
    theme: { primaryColor: "#7c3aed" },
    locale: "en"
  });
</script>

The stub queue lets Boarbs.init(...) run before widget.js finishes loading — important when the loader is async.

YOUR_BOARBS_DOMAIN means the domain that serves your Boarbs instance or workspace. It is not the customer's app domain where you paste the snippet. On Boarbs Cloud this is typically a Boarbs-hosted workspace URL such as acme.boarbs.com. On a self-hosted install it is your own Boarbs domain.

How Workspace Resolution Works

  • If your script loads from a workspace domain such as acme.boarbs.com, the widget can resolve the workspace automatically.
  • If you want to be explicit, pass workspace: "acme".
  • If you want the widget pinned to one specific public board, pass boardId: "...".
  • If you pass a sessionToken, the widget resolves the board from that session. If the session was created without a boardId, Boarbs pins it to the first public board in the API key's workspace.

The widget renders a single feedback board inside a slide-in panel in your app. It uses the board from the sessionToken when present, the explicit boardId when provided, or the first public board in the workspace as a fallback.

When Changelog is enabled for the workspace, the panel also includes a Changelog tab.

The Two Modes

Launcher mode (default)

A floating pill in the corner opens the panel. Visible on every page where the script runs. Use this when you want feedback to be discoverable by default.

window.Boarbs.init({ workspace: "acme", mode: "launcher" });

Trigger mode

No floating button. The panel is opened programmatically from your own UI — a "Feedback" link in your user menu, a button in your settings page, anywhere.

window.Boarbs.init({ workspace: "acme", mode: "trigger" });

// In your own click handler:
document.getElementById("feedback-link").addEventListener("click", function () {
  window.Boarbs.open();
});

You can switch modes by calling init() again with different config. The widget is idempotent.

Anonymous vs Identified

Anonymous (default)

If you don't pass a session token, the widget runs anonymously. Visitors enter their email when they submit feedback, vote, or comment. The email is remembered in localStorage for the rest of the session, so they only enter it once.

This is the right mode for marketing sites, landing pages, or when you don't have user authentication.

Identified

Pass a sessionToken to attribute every action to a specific user — votes, comments, and posts are linked to their identity, and the email field disappears from the form.

The session token is created by your backend with your API key. Never put your API key in browser code.

// On your server:
const response = await fetch("https://YOUR_BOARBS_DOMAIN/api/embed/sessions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BOARBS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    boardId: process.env.BOARBS_BOARD_ID, // optional, but recommended when you have multiple public boards
    userId: user.id,
    email: user.email,
    firstName: user.firstName,
    lastName: user.lastName,
    avatarUrl: user.avatarUrl,
    company: { id: "account-123", name: "Acme Inc." },
    mrr: { amount: 149.99, currency: "USD" },
    plan: user.plan,
    expiresInSeconds: 2592000,
  }),
});

const { sessionToken } = await response.json();

If you omit boardId, the session is created against the first public board in the API key's organization.

When you create identified sessions, use a stable userId whenever possible. Boarbs prefers userId over email for matching returning users and falls back to email when needed.

This is the recommended pattern:

  • send userId for every identified user
  • send email as well when available
  • keep userId stable even if the user later changes their email address
  • pass avatarUrl when you want the widget to show the user's avatar

avatarUrl is optional. If you do not want profile images in the widget, simply omit it. If you do pass one, prefer a first-party or proxied image URL if privacy matters in your environment.

Pass it into the widget at init, or after sign-in:

// At init:
window.Boarbs.init({ workspace: "acme", sessionToken: "..." });

// After a user signs in mid-session:
window.Boarbs.identify("...");

The session token is the same one used by the iframe Embed flow — same endpoint, same fields, same lifetime rules.

Identifying a widget user also does not create a portal sign-in account by itself. To open the hosted portal as that same person, use Automatic Login — the widget attaches ?sso= to portal links, and Boarbs.portalUrl(path) builds those URLs for you.

JavaScript API

CallBehavior
Boarbs.init(config)Mount or reconfigure. Idempotent.
Boarbs.open(tab?)Open the panel. Optional "feedback", "posts", or "changelog".
Boarbs.openPost(postId, { upvote? })Open a post directly. Set upvote: true to add the current user's vote only when it does not already exist.
Boarbs.close()Close the panel.
Boarbs.toggle(tab?)Toggle.
Boarbs.identify(token)Switch to identified mode at runtime.
Boarbs.portalUrl(path?)Hosted portal URL with ?sso= when a session token is present.
Boarbs.destroy()Remove launcher and panel from the DOM.
Boarbs.hasUnseenChangelog()true while the launcher unread badge is shown.
Boarbs.on(event, fn)Subscribe to ready | open | close | submit | error | unread | boosterShown | boosterDismissed.

Lifecycle Callbacks

Pass these into init():

window.Boarbs.init({
  workspace: "acme",
  onOpen: () => console.log("opened"),
  onClose: () => console.log("closed"),
  onSubmit: (post) => console.log("submitted", post),
});

Open a Tab Directly

window.Boarbs.open("posts");
window.Boarbs.open("changelog");

Boarbs.open("changelog") opens the Changelog tab when the module is on.

Changelog unread

Admins and owners can limit the unread badge under Settings → General → Changelog unread badge (/app/settings/changelog): Everyone (default), Identified users only (user ID or email in a valid embed session), or Specific plans (identified sessions with a matching stored plan). Enter comma-separated plan names; values are trimmed and deduplicated, with at most 50 values of 100 characters each. Matching is case-sensitive (Pro differs from pro). An empty plan list matches nobody and shows a settings warning.

Identity and plan come from the server-side EmbedSession created through identify/embed; unread query parameters cannot override them. Excluded visitors receive hasUnseen: false, unseenCount: 0, and audienceMatched: false, which hides the launcher badge. Changelog content stays visible and opening it still records last-seen state. Role segments and per-entry segmentation are later cuts. See Unread badge audience.

When Changelog is on, the launcher shows a red count badge for unseen published entries: 1–9, then 9+. At zero, the badge is hidden. Opening Changelog — the tab or Boarbs.open("changelog") — resets the count to zero. Identified sessions keep their server last-seen state; anonymous visitors keep it locally.

window.Boarbs.on("unread", ({ hasUnseen, count }) => {
  // hasUnseen: boolean; count: number (capped at 100)
  // count is null when an older API supplies only hasUnseen (red dot fallback).
});

window.Boarbs.hasUnseenChangelog(); // boolean

Changelog booster

The optional booster is off by default. Admins and owners enable Show changelog booster in Changelog settings. It follows the unread audience and shows the latest unseen published entry in a small card beside the launcher, without moving focus. The unread dot/count keeps its existing behavior. The card has no animation, including when reduced motion is preferred.

Read opens Boarbs.open("changelog") and marks Changelog seen. Closing the card leaves unread state intact. Each entry is shown once per browser and workspace; localStorage remembers shown/dismissed entry IDs, with a page-session memory fallback when storage is blocked. Cards are hidden while the panel is open and are removed by destroy(). Trigger mode shows no card.

Disable automatic cards on a particular host:

window.Boarbs.init({ workspace: "acme", changelogBooster: false });

Listen before init() to capture the first automatic display:

window.Boarbs.on("boosterShown", function ({ entryId }) {
  // The card was displayed. This does not mark the entry seen.
});
window.Boarbs.on("boosterDismissed", function ({ entryId }) {
  // The visitor used the close button. Unread state is unchanged.
});

Both events carry { entryId }. Reading or hiding the card when opening the panel does not emit boosterDismissed. The unread API adds a booster object with entryId, plain-text title, nullable label, and public portal url, or null when there is no eligible unseen entry. Full-screen modals, carousels, frequency/schedule rules, and per-entry controls are later cuts.

Roadmap tab

Roadmap is opt-in so existing embeds keep their current tabs. Add "roadmap" to tabs:

window.Boarbs.init({
  workspace: "acme",
  tabs: ["posts", "roadmap"],
  defaultTab: "roadmap",
});

window.Boarbs.open("roadmap");

The tab is automatically hidden when the workspace has no Everyone roadmap. If the requested default tab is hidden, the widget uses the first available tab. open("roadmap") switches to the tab only when it is enabled in tabs and available.

The first public roadmap in workspace order is selected initially. A small selector lets visitors switch between multiple public roadmaps. Columns stack vertically on mobile; cards show the title, vote count, board, and status. Clicking a card opens the post detail inside the widget, where visitors can vote and comment. Empty roadmaps show an empty state.

Only posts on public boards that have passed approval appear; drafts, spam, deleted posts, private boards, and team-only roadmaps stay hidden. Roadmap filters, column visibility, and sorting follow the public portal. There is no roadmap unread badge or automatic popup. Changelog unread counts and boosters keep their existing behavior.

Deep-link to a Specific Post

Open the widget straight on a post from a button in your app. Pass the post's Boarbs ID:

window.Boarbs.openPost("POST_ID");

For an identified user, you can also ensure it is upvoted. This is idempotent: if that user has already voted, the existing vote stays in place. Anonymous visitors need a previously remembered email before a vote can be attributed.

window.Boarbs.openPost("POST_ID", { upvote: true });

Customization

OptionDefaultNotes
workspacescript domain's workspaceWorkspace slug. Optional when widget.js is already served from your workspace domain, or when a sessionToken is present. Used as the fallback board lookup scope when boardId is omitted.
boardIdfirst public boardExplicit public board target. Recommended when a workspace has more than one public board.
mode"launcher""launcher" shows the floating pill, "trigger" doesn't.
position"bottom-right"Or "bottom-left". Launcher mode only.
sessionTokennoneIdentified mode. Anonymous when omitted.
theme.primaryColorworkspace brand colorHex string. Drives the pill, panel accent, focus ring.
launcher.icon"default""default" | "chat" | "lightbulb" | URL to a 24×24 SVG/PNG.
launcher.label"Feedback"Text shown on the pill (also used for aria-label and tooltip).
launcher.showLabeltrueSet to false for an icon-only circular launcher.
tabs["posts"] + Changelog when enabledAccepts "feedback", "posts", "changelog", and "roadmap". Roadmap is opt-in and hidden without a public roadmap. Changelog is still added automatically when its module is enabled.
defaultTab"posts"First-open intent: "feedback", "posts", "changelog", or "roadmap". Must be an available tab; otherwise the first available tab is used. Include "roadmap" in tabs to select it.
changelogBoosterenabled only when the workspace enables itSet to false to suppress automatic changelog cards on this host. Does not change the unread dot/count.
localeworkspace Public language, then existing fallbackExplicit widget locale takes precedence over Settings → General → Public language. English (en) and German (de) localize the panel interface and changelog dates. BCP-47 values still set the panel’s <html lang> as before. The portal footer language cookie does not override the widget.

Reconfigure at Runtime

init() is idempotent. Calling it again with new config rebuilds the launcher and reloads the panel — useful when your app's theme changes after sign-in.

// Initial:
window.Boarbs.init({ workspace: "demo", theme: { primaryColor: "#7c3aed" } });

// User toggles dark mode:
window.Boarbs.init({ workspace: "demo", theme: { primaryColor: "#a78bfa" } });

Identifying Users After Sign-In

If your visitor signs in while the widget is already running, hand the new session token to the widget — no page reload needed:

// After your sign-in flow returns a Boarbs session token from your backend:
window.Boarbs.identify(sessionToken);

The panel reloads with the user's identity attached. The email field disappears from the feedback form, and votes / comments / posts attribute to that user.

React Example

A small client component that wires up the widget on mount:

"use client";

import Script from "next/script";
import { useState } from "react";

declare global {
  interface Window {
    Boarbs?: {
      init: (config: Record<string, unknown>) => void;
      open?: (tab?: string) => void;
      openPost?: (postId: string, options?: { upvote?: boolean }) => void;
    };
  }
}

export function BoarbsWidget({ workspace }: { workspace: string }) {
  const [loaded, setLoaded] = useState(false);

  return (
    <Script
      src="https://YOUR_BOARBS_DOMAIN/widget/v1/widget.js"
      strategy="afterInteractive"
      onLoad={() => {
        window.Boarbs?.init({
          workspace,
          mode: "launcher",
          position: "bottom-right",
        });
        setLoaded(true);
      }}
    />
  );
}

Content Security Policy

If your site uses a CSP, allow the widget origin in three directives:

script-src  'self' YOUR_BOARBS_DOMAIN;
frame-src   'self' YOUR_BOARBS_DOMAIN;
connect-src 'self' YOUR_BOARBS_DOMAIN;

The panel iframe is sandboxed (allow-scripts allow-same-origin allow-forms allow-popups). The loader's only DOM footprint is a single launcher button and a single iframe container — it never reads or modifies the rest of your page.

Changelog custom field filters

The Changelog tab shows compact selects for the workspace's public custom fields, alongside the existing New / Improved / Fixed label filter. Select one option per field; multiple fields and the label are AND-combined. All options clears a field. Public selected values appear on entries; private fields stay inside the workspace. An empty result keeps the controls available so visitors can change their selections. These content filters do not change unread counts, last-seen tracking, or the booster. Configure fields in Changelog settings.

Versioning

The script URL pins to a major version: /widget/v1/widget.js. Non-breaking changes ship in place — keep your snippet exactly as written and you'll receive improvements automatically. A breaking change ships under a new path (/v2/...).

Performance

  • Loader: under 15 KB gzipped.
  • Iframe: loads only on first open. Visitors who don't click pay zero panel weight.
  • Mobile (under 640px viewport): full-screen panel using 100dvh to dodge iOS Safari's address bar.
  • prefers-reduced-motion: animations are skipped at runtime, not via CSS — so they're truly off, including hover.
  • @media print: launcher and panel are hidden.

Accessibility

  • Launcher: aria-label, aria-haspopup="dialog", focus ring with brand-tinted color.
  • Panel: role="dialog", aria-modal="true", labelled by the workspace name.
  • Keyboard: opens with Enter/Space on the launcher; ESC closes the panel and returns focus to the launcher; Tab order is contained inside the iframe.

Troubleshooting

The launcher doesn't appear. Check the browser console. Confirm the script URL is reachable, your CSP allows it, and the widget can resolve the workspace either from the script domain, workspace, or sessionToken.

The panel opens but is blank. The workspace lookup failed, or the workspace has no public boards. Check the workspace slug or the Boarbs domain you're loading from.

Voting and commenting don't work. For anonymous visitors, vote and comment require an email address — the widget prompts for one on first interaction and stores it in localStorage. If your CSP blocks connect-src to the widget domain, the requests will silently fail.

The widget shows "Session expired". Your session token has expired. Either request a new one from your backend and call Boarbs.identify(newToken), or fall back to anonymous mode by removing sessionToken from init().

A returning identified user cannot undo an old vote. Check that your backend is issuing the same stable userId as before. Boarbs prefers userId for returning-user matching and only falls back to email when needed.

Multiple init() calls create duplicates. They shouldn't — init() is idempotent. If you see a duplicate launcher, you've loaded widget.js twice. Check that only one script tag points at it.

When to Use the Widget vs Other Options

Use the widget when:

  • You want feedback discoverable on every page of your product
  • You want install time measured in minutes
  • You're fine with the panel being a corner sheet (not a full page)

Use the Embed iframe when:

  • You want a dedicated "Feedback" page inside your app at full size
  • You want to pass URL filters (status, search, sort) on initial load
  • You're building a deeper integration that needs server-rendered embed sessions

Use the Portal when:

  • You want zero install — Boarbs hosts the page at your-workspace.boarbs.com
  • You want SEO and a public roadmap as part of your marketing surface

Next Steps

  • Embed — Full-page iframe with server-created sessions
  • Automatic Login — Carry widget identity onto the hosted portal
  • Portal — The hosted public site
  • API — Reference for the /api/embed/sessions endpoint and the rest of the public API
  • Permissions and Visibility — How public boards, embed sessions, and identified users interact
  • Troubleshooting — Common problems and where to look

Company and monthly recurring revenue

POST /api/embed/sessions accepts optional company and mrr objects:

  • company.name is required when company is supplied; company.id is optional. Both must be strings of 1–200 characters.
  • mrr.amount must be a finite number from 0 to 1000000000, with at most two decimal places. mrr.currency is an uppercase ISO-4217 currency code and defaults to USD.
  • Invalid values return HTTP 400. Unknown properties inside either object are rejected. Null is not an omission.

Company and MRR are team-only: workspace members see them in the portal users list and beside post authors and voters. Public portals, widget post details, comments, votes, and browser-facing session responses do not include them. Keep the API key and this customer context on your backend.

Customer profile sync requires email. Identity-only sessions keep their existing behavior. Immediately after an embed session is created with company or mrr, Boarbs attempts one server-side customer sync. Page loads, widget identification, navigations, and refreshes do not repeat it. Automatic login continues to sync the stored customer context. A supplied company or MRR replaces that person's existing value in this workspace. Omitting either object preserves its existing value. A company supplied without id replaces the name and clears the previous company ID. Zero MRR is a supplied value. Amounts are stored as integer hundredths of the currency unit and formatted for the workspace viewer's locale; no currency conversion is performed. Session-creation sync skips workspace members and does not create a login cookie or grant workspace membership. If that sync fails, Boarbs logs a generic message without the payload and still returns the session. Automatic login can sync the stored context later.

Boarbs© 2026 Boarbs