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, and CHAINLOG_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.js and calls ChainlogWidget.init.
  • Product or ops teams can keep changing widget defaults in Chainlog without editing host code when useTenantDefaults is 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.

  1. Create a widget credential in Dashboard → Widget and store the secret immediately.
  2. Confirm the backend route returns a JWT, not the credential secret.
  3. Load the page, open the widget, and confirm updates appear for the signed-in user.
  4. Refresh the page and confirm the widget still loads without manual credential changes.
  5. 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-src and frame-src CSP rules.
  • Expecting inline mode to work without target or a data-chainlog-widget container.
  • Trying to decide audience rules in frontend conditionals instead of JWT claims and entry metadata.

What to read next