Framework guides
Keep framework docs thin and let the shared runtime model do the heavy lifting.
Chainlog does not need a different mental model per stack. Use the plain JavaScript runtime everywhere, then adapt how you load the script, mount the button, and expose the backend token route.
Plain JavaScript
This is the canonical runtime. Other framework examples should stay close to this shape.
<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><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>React
A React host component only needs to ensure the loader exists, initialize once, and destroy on unmount.
useEffect(() => {
ensureWidgetApi();
ensureWidgetScript();
window.ChainlogWidget?.init({
tenantId: "CHAINLOG_TENANT_ID",
useTenantDefaults: true,
tokenProvider: async () => {
const response = await fetch("/api/widget-token", { credentials: "include" });
return response.json();
},
});
return () => window.ChainlogWidget?.destroy();
}, []);
return (
<button type="button" onClick={() => window.ChainlogWidget?.toggle()}>
Release notes
</button>
);SSR and hydration note
Keep widget initialization inside useEffect or an equivalent client-only hook. The widget runtime expects window, document, and an iframe mount target.
Next.js and backend token routes
The framework-specific part is usually the token endpoint, not the widget runtime.
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 });
}- Next.js route handlers are the most direct path if your product already runs on Next.
- Express, Rails, and Django examples in the repo follow the same JWT contract and can be adapted directly.
- Whatever the stack, the backend should return a short-lived token plus expiry metadata for refresh scheduling.
What we document today
This page intentionally documents the wrappers and examples that actually exist in the current codebase.
- Plain JavaScript runtime snippets for launcher, panel, and inline embeds.
- React host example for native app buttons and lifecycle-safe initialization.
- Next.js, Express, Rails, and Django backend token route examples.
- For other frameworks, start from the plain JavaScript runtime plus the JWT contract on the auth page.
What to read next