Widget runtime
Treat the browser embed as a small, stable runtime surface.
The widget loader exposes a queue-based global API, a single singleton iframe instance, token refresh support, and an event bridge for host analytics and UI coordination.
Use this guide when you already know the widget is part of the workflow
This page is for engineers and platform teams who need exact runtime behavior after Quickstart, not for teams that are still deciding whether they need the widget at all.
Best for host-app control
Use this when your app owns the trigger, layout, page placement, or analytics hooks around the widget.
Best for richer embed patterns
Use this when you need inline changelog blocks, module-scoped views, or host-controlled panel behavior.
Not the first stop
If your only goal is to get something live, Quickstart is still the better path. Come here when implementation details start to matter.
Minimal runtime call
Start with tokenProvider and tenant defaults. Only override presentation fields when the host app truly owns that decision.
window.ChainlogWidget.init({
tenantId: "CHAINLOG_TENANT_ID",
useTenantDefaults: true,
tokenProvider: async () => {
const response = await fetch("/api/widget-token", {
credentials: "include",
});
const { token, expiresInSeconds } = await response.json();
return { token, expiresInSeconds };
},
});Most common widget workflows
Most teams use the same runtime in a few repeatable ways.
Launcher or panel for global updates
Best when updates should be available from anywhere in the product with one consistent entry point.
Inline changelog for a specific page
Best when onboarding, settings, billing, or admin pages should show release notes in-place instead of in a floating shell.
Host-owned triggers and analytics
Best when your app already has command surfaces, navigation, or event tracking you want the widget to plug into cleanly.
Configuration reference
These are the public config fields that matter for most implementations.
| Field | Required | Behavior |
|---|---|---|
| tenantId | Recommended | Required when using tenant runtime defaults. Identifies which tenant runtime config to fetch. |
| token | Optional | Short-lived widget JWT. Use only when you are not using tokenProvider. |
| tokenProvider | Optional | Async function that returns a token string or { token, expiresInSeconds | expiresAt }. |
| useTenantDefaults | Optional | When true, loads tenant-managed defaults for mode, theme, trigger, label, sizing, and max items. |
| mode | Optional, default "launcher" | Supported values are "launcher", "panel", and "inline". |
| showTrigger | Optional, default true | Hide the built-in trigger when your host app owns the button. |
| position | Optional, default "bottom-right" | Floating position for launcher and panel modes. |
| title / subtitle | Optional | Override the in-widget header copy. |
| width / panelWidth / triggerWidth | Optional | Dimension overrides for floating layouts. Strings or numbers are supported. |
| target | Inline only | Selector or mount target for inline mode. Required if there is no [data-chainlog-widget] container. |
| autoHeight | Inline only | Defaults to true. Resize messages from the frame adjust height automatically. |
| contextModule | Optional | Pass a product-area trigger such as billing or dashboard to filter visible entries. |
| onEvent | Optional | Receives widget lifecycle, click, filter, height, and error events. |
Runtime methods
The runtime surface is intentionally small. Most behavior changes happen through config and entry metadata.
| Method | Purpose |
|---|---|
| init(config) | Create the singleton widget instance and mount the iframe. |
| update(partialConfig) | Merge new config, optionally refresh tokenProvider, and repost config to the frame. |
| destroy() | Remove the iframe instance and clear token refresh timers. |
| open() | Set open state to true for launcher or panel mode. |
| close() | Set open state to false for launcher or panel mode. |
| toggle() | Flip the current open state for launcher or panel mode. |
There is only one widget instance per page
Calling init again updates the existing singleton. Use update for deliberate runtime changes and destroy when unmounting in SPA transitions.
Events and payloads
Use onEvent for analytics, host UI updates, and debugging. Keep handlers resilient because they run in your app shell.
window.ChainlogWidget.init({
tenantId: "CHAINLOG_TENANT_ID",
tokenProvider,
onEvent(payload) {
if (payload.type === "entry_clicked") {
analytics.track("release_note_opened", {
entryId: payload.entryId,
slug: payload.slug,
deliveryType: payload.deliveryType,
});
}
if (payload.type === "filter_changed") {
console.info("Visible entries", payload.visibleCount);
}
},
});| Event | Common payload fields | When it fires |
|---|---|---|
| launcher_toggle / panel_toggle | open, mode | Fires when the floating surface opens or closes. |
| data_loaded | count, totalLoaded, hasMore, nextCursor, contextModule | Fires on initial load and load more. |
| filter_changed | deliveryFilter, categoryFilter, visibleCount, totalLoaded | Fires when widget filters change. |
| entry_clicked | entryId, slug, title, deliveryType, changeCategory, releaseStatus | Fires when an entry opens inside the widget. |
| load_more_clicked | nextCursor, loadedCount | Fires before the widget fetches the next page. |
| content_height | height | Inline mode only. Lets the host resize the iframe. |
| retry_clicked / error | message, mode | Fires on retry attempts and runtime errors. |
Common layouts
The same runtime supports host-owned triggers and inline embeds without a separate SDK.
Host-owned button
<button type="button" onclick="window.ChainlogWidget.toggle()">
Release notes
</button>
<script>
window.ChainlogWidget.init({
tenantId: "CHAINLOG_TENANT_ID",
useTenantDefaults: true,
tokenProvider: async () => fetch("/api/widget-token", { credentials: "include" }).then((r) => r.json()),
mode: "panel",
showTrigger: false,
launcherLabel: "Updates",
});
</script>Inline changelog block
<div id="release-notes"></div>
<script>
window.ChainlogWidget.init({
tenantId: "CHAINLOG_TENANT_ID",
useTenantDefaults: true,
tokenProvider: async () => {
const response = await fetch("/api/widget-token", { credentials: "include" });
return response.json();
},
mode: "inline",
target: "#release-notes",
title: "Release notes",
subtitle: "What changed recently",
autoHeight: true,
});
</script>Errors, token refresh, and sizing behavior
These behaviors are easy to miss when reading code casually, so document them explicitly.
- If you provide tokenProvider and expiry metadata, the loader schedules token refresh before expiry.
- Recoverable auth errors trigger a forced token refresh attempt when tokenProvider is available.
- Inline mode sends content height events and auto-resizes unless you set a fixed height or disable autoHeight.
- Inline mode requires target or a
data-chainlog-widgetcontainer once the page is mounted. - Widget entry fetches currently rate-limit at 120 requests per minute per tenant and widget subject.
What to read next