In-app messaging 2.0
The schema-driven in-app engine — sixteen surfaces, twelve trigger types, inline slots, and a headless renderer for messages you drive yourself.
In-App Messaging 2.0 is the schema-driven engine: a message is a variant — a
surface, a set of content blocks, targeting, triggers and frequency rules — and
the SDK renders it. It is a separate authoring path from the legacy in-app
campaigns that the engage feature delivers, and the two are disjoint, so a
message is delivered by one or the other and never both.
It is off by default:
await ScaleBun.init({ clientKey, apiBaseUrl, features: { inapp2: true } });Surfaces#
Sixteen, because "modal" is not one shape:
modal · slideover · panel · popover · coachmark · tour · hotspot ·
inline · banner · toast · center · launcher · palette · checklist ·
takeover · toolbar
Each carries its own zone, focus behaviour and cost, so a toast cannot steal
focus and a takeover is not shown alongside three other things.
Triggers#
manual · route · event · idle · element · hover · selection ·
keyboard · right-click · schedule · no-data · exit-intent
Configured per campaign in the dashboard. event fires from your own
ScaleBun.track() calls, so no extra instrumentation is needed.
Inline slots#
Surfaces that belong in the page rather than over it need somewhere to mount. Register a slot whose name matches the campaign's placement key:
import { InAppHeadless } from '@scalebun/web/inapp';
const inapp = new InAppHeadless();inapp.setTheme('auto');inapp.registerSlot('empty-state', el);Each framework has a component or directive for this — <InAppSlot> in React,
useInAppSlot in Vue, use:inAppSlot in Svelte, scalebunInAppSlot in
Angular. See Integrations.
Rendering a variant yourself#
InAppHeadless renders a variant you already hold — for custom UI, or a preview.
It needs no feature flag, because it renders what you hand it rather than
fetching campaigns.
import { InAppHeadless, validateVariant } from '@scalebun/web/inapp';
const inapp = new InAppHeadless();if (validateVariant(variant).length === 0) { await inapp.render(variant); // hooks are optional and default to no-ops}Pass hooks to wire your own analytics:
await inapp.render(variant, { onImpression() {}, onView() {}, onCtaClick() {}, onInteraction() {}, onDismiss() {},});Heavy content blocks#
Markdown, code, chart, table and video blocks are loaded on demand so they never reach the core bundle. If you render a variant that uses one directly, ensure them first:
import { ensureHeavyBlocks, isHeavyBlock } from '@scalebun/web/inapp';
await ensureHeavyBlocks();The engine does this for you; it only matters on the headless path.
The registry#
WEB_REGISTRY is the canonical catalogue of variants the web SDK can render,
with getVariantById and getVariantByKey lookups, and WEB_CATALOG for the
full set. These are what keep the dashboard's authoring options and the SDK's
rendering capability from drifting apart.
Legacy in-app campaigns#
The engage feature still delivers the original four inline formats —
inline_card, embedded_panel, empty_state_prompt, smart_inbox_message —
through a different slot API on the facade:
const unregister = await ScaleBun.registerInAppSlot('empty-state', el);Without a matching slot those fall back to a modal. If you are running both systems, note that the slot names live in separate registries.
Gamified formats#
Eight formats hand out a reward, and the SDK deliberately has no randomness in it — a browser that picks its own prize can be made to pick again. Supply a resolver and your backend grants or reserves the reward:
await ScaleBun.init({ clientKey, apiBaseUrl, engageOptions: { onInAppGameRequest: async (request) => fetch('/api/reward', { method: 'POST', body: JSON.stringify(request), }).then((r) => r.json()), },});Without it, those formats render their unavailable state rather than a wrong prize.
Next#
Engagement — surveys, NPS and push.
Coachmarks & tours — the anchored surfaces.