Quickstart
Get the widget working in production with three pieces.
For most teams, the setup is simple: create a widget credential, mint a short-lived token on your backend, and initialize the widget in your app. Your secret stays on the server the whole time.
Use this guide when the job is simple
This is the right path when you want release notes live inside your product quickly and you do not need to think about advanced targeting, analytics, or custom layouts yet.
Best for frontend teams
Use this when the immediate goal is to get a working launcher or panel into your app with the fewest moving parts.
Best for product teams
Use this when you already have entries to publish and now need a safe in-product surface for customers to read them.
Not for every workflow
If you already know you need exact runtime behavior, segmentation rules, or automation, jump to Widget runtime, Identity, or API docs instead.
The three pieces you need
Quickstart is intentionally boring: one loader, one backend token route, and one init call.
1. Queue stub and loader
<script>
window.ChainlogWidget = window.ChainlogWidget || {
_q: [],
init: function(config) { this._q.push({ method: "init", config: config }); },
update: function(config) { this._q.push({ method: "update", config: config }); },
destroy: function() { this._q.push({ method: "destroy" }); },
open: function() { this._q.push({ method: "open" }); },
close: function() { this._q.push({ method: "close" }); },
toggle: function() { this._q.push({ method: "toggle" }); }
};
</script>
<script async src="https://chainlog.tech/widget.js"></script>2. Backend token route
import { SignJWT } from "jose";
export async function GET() {
const user = await getCurrentUserFromYourApp();
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
const now = Math.floor(Date.now() / 1000);
const secret = new TextEncoder().encode(process.env.CHAINLOG_WIDGET_CREDENTIAL_SECRET!);
const token = await new SignJWT({
tenant: process.env.CHAINLOG_TENANT_ID!,
sub: user.id,
aud: "chainlog-widget",
kid: process.env.CHAINLOG_WIDGET_CREDENTIAL_ID!,
entitlements: user.entitlements || [],
contextModule: user.contextModule || null,
internalAccess: Boolean(user.canViewInternalUpdates),
iat: now,
exp: now + 600,
})
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.sign(secret);
return Response.json({ token, expiresInSeconds: 600 });
}3. Frontend init call
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 };
},
});What each team is responsible for
The framework is not the important decision. The security boundary is.
- The backend owns
CHAINLOG_WIDGET_CREDENTIAL_SECRET,CHAINLOG_WIDGET_CREDENTIAL_ID, andCHAINLOG_TENANT_ID. - The backend checks the user's normal app session, signs a short-lived widget JWT, and returns that token to the frontend.
- The frontend loads
https://chainlog.tech/widget.jsand callsChainlogWidget.init. - Product or ops teams can keep changing widget defaults in Chainlog without editing host code when
useTenantDefaultsis enabled.
Recommended defaults
Start with useTenantDefaults: true and a widget JWT lifetime of about 10 minutes. That keeps the host code small while still giving you room to change labels, mode, theme, and sizing later.
How to know you are done
A teammate should be able to verify the integration without needing the rest of the docs.
- Create a widget credential in Dashboard → Widget and store the secret immediately.
- Confirm the backend route returns a JWT, not the credential secret.
- Load the page, open the widget, and confirm updates appear for the signed-in user.
- Refresh the page and confirm the widget still loads without manual credential changes.
- Only reach for browser devtools if you need to confirm the widget is calling your token route.
Common first-launch mistakes
Most early issues come from one of these workflow assumptions, not from a widget bug.
- Passing the credential secret to the browser instead of a short-lived JWT.
- Using a fixed token with no refresh path for long-lived sessions.
- Forgetting to allow Chainlog in strict
script-srcandframe-srcCSP rules. - Expecting inline mode to work without
targetor adata-chainlog-widgetcontainer. - Trying to decide audience rules in frontend conditionals instead of JWT claims and entry metadata.
What to read next