ScaleBun
Skip to article

Message inbox

webDeveloper

A persistent, offline-first notification centre — a headless controller you can render yourself, or a built-in bell and panel that drops into an existing navbar.

Updated Reviewed

A message inbox outlives the moment a notification arrives. Push and in-app messages are interruptions; the inbox is where they persist, so someone who dismissed a message on Monday can still find it on Thursday.

It is off by default. Enable it at init:

TypeScript
await ScaleBun.init({ clientKey, apiBaseUrl, features: { inbox: true } });

With the feature off, ScaleBun.inbox() returns a safe no-op controller — every method does nothing rather than throwing. That is deliberate, but it means a forgotten flag looks exactly like an empty inbox.

Headless or rendered#

The controller is headless first. The built-in UI is opt-in, so a host that renders its own list never pays for the widget.

TypeScript
const inbox = ScaleBun.inbox();
// Render your ownconst unsubscribe = inbox.subscribe((state) => {  setUnread(state.unreadCount);  setSyncing(state.syncStatus === 'syncing');});
const page = await inbox.query({ limit: 20 });await inbox.markRead(page.messages[0].id);

Or mount the built-in panel:

TypeScript
inbox.mount({  trigger: document.querySelector('#bell')!,  // your own bell, or omit for the floating one  layout: 'dropdown',                          // 'drawer' (default) | 'dropdown'  accent: '#6c5ce7',  onViewAll: () => router.push('/notifications'),});
OptionNotes
triggerCSS selector or element to use as the open/close toggle.
layoutdrawer is a full-height slide-over; dropdown is a compact popover.
positionCorner for the built-in bell. Default bottom-right.
themeForce light/dark. Default follows prefers-color-scheme.
accentBrand colour for bell, badge, unread dot and CTAs.
onViewAllRenders a footer button and calls this. The SDK never navigates — your routes are yours.

The controller#

GroupMethods
Statesubscribe · onUnreadChange · getState
Readquery · get
MutatemarkRead · markUnread · markAllRead · archive · unarchive · pin · remove
Actionsact · onCtaAction
Syncrefresh · ingest
UImount · open · close · unmount

subscribe and onUnreadChange both fire immediately with the current value, then on every change — so a badge does not need a separate first read.

act(id, ctaKey) records the click and hands it to your onCtaAction handler. As with onViewAll, the SDK does not navigate for you.

Offline-first#

Messages are stored locally and synced by delta, so the list renders instantly on a cold load and survives a lost connection. syncStatus on the state object tells you whether a refresh is in flight, which is what a spinner should key off rather than the presence of messages.

Use ingest() to push messages you already hold — a server-rendered payload, or a message delivered by your own channel — into the same store.

Theming#

The panel reads --scalebun-inbox-* CSS custom properties, and accent on mount() overrides the accent token directly.

All user-facing strings are overridable, and every one is rendered with textContent, never innerHTML — these can come from a dashboard field, and interpolating them as markup would put an injection point into every host page. Count-bearing labels are functions (unread: (n) => …) so your own Intl.PluralRules decides plurals rather than receiving "1 unread".

Events#

Eleven reserved events are emitted, all beginning $message_ plus $inbox_unread_changed. See Reserved events before you name your own.

Next#

Message inbox · Web SDK · ScaleBun