Sign in →
Embed Widgets1 min read

Embed Widgets: Integration Patterns

End-to-end worked example, common pitfalls, production checklist, migration paths, and concurrent-widget shared auth state.

Updated 2026-06-15Suggest edits
Docs Embed Widgets Integration Patterns

End-to-end: "Acme Corp" example#

Acme Corp sells a developer API. They want to:

  1. Show pricing on their public marketing page
  2. Let signed-in customers self-serve subscribe
  3. Let signed-in customers view and pay invoices

Step 1 — Operator setup (one-time)

  • Sign up at app.aforo.ai
  • Configure brand kit (logo, primary color) under Storefront → Customize
  • Publish at least one offering with a rate plan
  • Open Embed Studio → Keys → Mint a new embed key. Add allowed domains:
    • https://acme.com (marketing)
    • https://app.acme.com (portal)
    • https://staging.acme.com (staging)
  • Open Embed Studio → Auth Bridge → Mint a NEXT signing key. Promote it to ACTIVE. Store the secret in your secret manager.

Step 2 — Pricing page (anonymous)

Drop a PricingCard on acme.com/pricing. No bridge token needed.

acme.com/pricing.html
<script
  src="https://embed.aforo.ai/v1/loader.js"
  integrity="sha384-…"
  crossorigin="anonymous"
  async
></script>

<div
  data-aforo-widget="pricing-card"
  data-tenant-slug="acme"
  data-embed-key="embk_live_…"
  data-layout="horizontal"
  data-featured-offering-id="off_pro"
></div>

Step 3 — Subscribe button on the portal

After the customer signs in on app.acme.com, the portal mints a bridge token server-side and renders the button:

app.acme.com/subscribe/page.tsx
import { AforoSubscribeButton } from '@aforoai/storefront-widgets';
import { mintBridgeToken } from '@/lib/aforo';

export default async function SubscribePage({ searchParams }) {
  const session = await getSession();
  if (!session) redirect('/login');

  const bridgeToken = await mintBridgeToken({
    externalId: session.customerId,
    email: session.email,
  });

  return (
    <AforoSubscribeButton
      tenantSlug="acme"
      embedKey="embk_live_…"
      bridgeToken={bridgeToken}
      offeringId={searchParams.plan ?? 'off_pro'}
      mode="redirect"
      returnUrl="https://app.acme.com/welcome"
    />
  );
}

Step 4 — Invoice list on the account page

app.acme.com/account/invoices/page.tsx
import { AforoInvoiceList } from '@aforoai/storefront-widgets';

export default async function InvoicesPage() {
  const session = await getSession();
  const bridgeToken = await mintBridgeToken({
    externalId: session.customerId,
    email: session.email,
  });

  return (
    <AforoInvoiceList
      tenantSlug="acme"
      embedKey="embk_live_…"
      bridgeToken={bridgeToken}
      defaultStatus="unpaid"
    />
  );
}

Step 5 — Webhook handler for the source of truth

Set up a webhook receiver on app.acme.com/api/aforo/webhook. When subscription.created fires, Acme writes to their database, provisions API keys, and sends a welcome email — all driven by the signed webhook, not the widget's postMessage event.

Common pitfalls#

PitfallFix
Provisioning access based on postMessage eventsUse signed webhooks as the source of truth. postMessage is advisory.
Forgetting to add staging domain to the embed key allowlistMint a separate dev/staging key. Production key should only contain production domains.
Long-lived bridge tokens (24h)Use 5-minute TTLs. The SDK auto-refreshes the session JWT, not the bridge token.
Same embed key across all environmentsMint one key per environment (production / staging / dev). Cleaner audit, faster rotation.
Missing CSP script-src directiveAdd https://embed.aforo.ai to your script-src. The widgets fail silently otherwise.
Forgetting noopener on Pay Now / PDF downloadThe SDK does this internally — but if you build your own actions wrapping these, use noopener,noreferrer.
Mounting widgets before the customer signs inThe widgets render an error state without a valid bridgeToken. Conditionally render after auth completes.
Bridge token signing key in source codeStore in HSM / KMS / secret manager. Rotate via NEXT → ACTIVE → RETIRED in Embed Studio.

Production readiness checklist#

Before flipping a widget to production, check off:

Embed key minted with production-only allowed domains (no localhost, no *.staging.com)
Bridge signing key promoted to ACTIVE; old NEXT key retired
Signing key stored in HSM / KMS / secret manager (not in environment variables or repo)
SRI hash pinned via integrity= attribute on the <script> tag
CSP includes script-src https://embed.aforo.ai and connect-src https://api.aforo.ai
Webhook receiver validates signature with HMAC SHA-256 + 5-minute replay window
Webhook handler is idempotent (handles same event redelivered)
Webhook handler updates persistent state, not postMessage events
Bridge token TTL is 5 minutes or less
Bridge token claims include external_customer_id + email + iat + exp + aud
Aforo brand kit configured (logo, primary color) — widgets render with your branding
Tested in production browser matrix (Chrome / Firefox / Safari latest + Mobile)
Tested with adblockers — embed.aforo.ai is not blocked by EasyList
Monitoring set up: alert on webhook 4xx/5xx, alert on widget error rate spike
Customer support runbook: how to debug "widget shows error" reports

Migration paths#

From headless API (T6)

If you're already consuming Aforo's headless API and rendering your own UI, the plugin tier lets you swap individual surfaces incrementally. Start with the lowest-touch one (PricingCard on a marketing page), then migrate the customer portal flows. The headless API stays available — you can mix tiers freely.

From managed storefront (T1)

If you're using Aforo's managed storefront and want more control over branding and routing, the plugin tier is the bridge. Keep the managed storefront for the flows you don't want to own, and embed plugin widgets for the surfaces you do.

Concurrent widgets — shared auth state#

Pages often mount multiple widgets at once — for example, a customer portal might show an InvoiceList plus a UsageMeter plus a SubscribeButton (upsell) on the same page.

The SDK shares one bridge exchange + one SSE connection per tenant across all mounted widgets. If three widgets all need the session JWT, the SDK fires one bridge-exchange HTTP request and shares the result. This is enforced by an internal promise-coalescing map keyed on tenant slug.

INFO
You don't need to do anything special to enable this — it's automatic. The only thing to remember: pass the same tenantSlug and embedKey to every widget on the page. Different slugs spawn different sessions.

See the for the session lifecycle (init → getSessionJwt → refresh → destroy).