Skip to content

Tier 2 — Headless / web component integration

Generated from the canonical integration guide

This page is generated from docs/integration/headless.md — the single source of truth shared with the public marketing site. Edit the canonical file, then run npm --prefix tools/integration-docs-derive run derive.

Package: @bc-subscriptions/storefront-webcomponent API surface reference: packages/storefront-webcomponent/README.md


Who this is for

Developers building custom headless storefronts on any framework — Next.js (non-Catalyst), Nuxt, SvelteKit, Remix, Hydrogen, plain HTML — or any framework-agnostic environment. Also the right tier for Stencil themes where you want direct DOM control over widget placement (rather than the automatic Script Manager injection of Tier 1).

The web component (<bc-subscriptions-widget>) wraps the same SvelteKit subscriber portal that Stencil uses, compiled to a custom element via Svelte 5's customElement target.


Prerequisites

  • Deployed apps/api Worker, with its base URL
  • BC store hash (settings.store_hash in Stencil; available from your BC storefront channel in headless)
  • At least one subscription plan configured in bc-subscriptions admin

Install

Option A — CDN <script> tag (no build step)

<script
  src="https://bc-subscriptions-cdn.pages.dev/v1/bc-subscriptions-widget.iife.js"
  async
></script>

Place this in your <head> or just before </body>. The IIFE auto-registers <bc-subscriptions-widget> on evaluation. No module bundler needed.

Option B — npm (bundler-aware consumers)

npm install @bc-subscriptions/storefront-webcomponent

Then import as a side-effect to register the custom element:

import '@bc-subscriptions/storefront-webcomponent';

The import registers the element synchronously. After the import, <bc-subscriptions-widget> is available anywhere in your JSX, Svelte templates, Vue templates, or plain HTML.


Minimal working example

Plain HTML / Stencil manual placement

<!doctype html>
<html>
  <head>
    <script
      src="https://bc-subscriptions-cdn.pages.dev/v1/bc-subscriptions-widget.iife.js"
      async
    ></script>
  </head>
  <body>
    <h1>Product — Gourmet Coffee Subscription</h1>

    <bc-subscriptions-widget
      data-store-hash="abc12345"
      data-api-base-url="https://subs-api.bigcommerce-testing-7727.workers.dev"
      data-bc-product-id="42"
    ></bc-subscriptions-widget>

    <!-- existing add-to-cart markup -->
  </body>
</html>

Replace abc12345 with your store hash and 42 with the BC product entity ID. If data-bc-product-id is omitted, the widget attempts to read window.BCData.product_attributes.id (available on BC storefronts).

React (Next.js, Remix, Hydrogen)

// components/SubscriptionWidget.tsx
'use client'; // Next.js App Router: this is a client component

import { useEffect } from 'react';

interface Props {
  storeHash: string;
  apiBaseUrl: string;
  bcProductId: number;
}

export function SubscriptionWidget({ storeHash, apiBaseUrl, bcProductId }: Props) {
  useEffect(() => {
    // dynamically import to avoid SSR evaluation of the custom element
    import('@bc-subscriptions/storefront-webcomponent');
  }, []);

  return (
    <bc-subscriptions-widget
      data-store-hash={storeHash}
      data-api-base-url={apiBaseUrl}
      data-bc-product-id={String(bcProductId)}
    />
  );
}
// app/products/[slug]/page.tsx
import { SubscriptionWidget } from '@/components/SubscriptionWidget';

export default async function ProductPage({ params }: { params: { slug: string } }) {
  const product = await getProduct(params.slug); // your existing data fetching

  return (
    <main>
      <ProductDetails product={product} />
      <SubscriptionWidget
        storeHash={process.env.NEXT_PUBLIC_BC_STORE_HASH!}
        apiBaseUrl={process.env.NEXT_PUBLIC_SUBS_API_URL!}
        bcProductId={product.entityId}
      />
    </main>
  );
}

TypeScript + React 18: You must declare the element in JSX.IntrinsicElements. React 19+ handles unknown custom elements natively.

// types/custom-elements.d.ts
declare namespace JSX {
  interface IntrinsicElements {
    'bc-subscriptions-widget': React.DetailedHTMLProps<
      React.HTMLAttributes<HTMLElement> & {
        'data-store-hash': string;
        'data-api-base-url': string;
        'data-bc-product-id'?: string;
        'data-initial-auth-token'?: string;
      },
      HTMLElement
    >;
  }
}

SvelteKit

<!-- src/lib/components/SubscriptionWidget.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';

  export let storeHash: string;
  export let apiBaseUrl: string;
  export let bcProductId: number;

  onMount(async () => {
    await import('@bc-subscriptions/storefront-webcomponent');
  });
</script>

<bc-subscriptions-widget
  data-store-hash={storeHash}
  data-api-base-url={apiBaseUrl}
  data-bc-product-id={String(bcProductId)}
/>

Vue 3

<!-- components/SubscriptionWidget.vue -->
<template>
  <bc-subscriptions-widget
    :data-store-hash="storeHash"
    :data-api-base-url="apiBaseUrl"
    :data-bc-product-id="String(bcProductId)"
  />
</template>

<script setup lang="ts">
import { onMounted } from 'vue';

const props = defineProps<{
  storeHash: string;
  apiBaseUrl: string;
  bcProductId: number;
}>();

onMounted(async () => {
  await import('@bc-subscriptions/storefront-webcomponent');
});
</script>

Add 'bc-subscriptions-widget' to compilerOptions.isCustomElement in vite.config.ts to suppress Vue warnings:

// vite.config.ts
export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag.startsWith('bc-subscriptions-'),
        },
      },
    }),
  ],
});

Attribute API

Attribute Type Required Default Notes
data-store-hash string yes BC store hash. Missing → visible error banner
data-api-base-url string yes Base URL of the deployed apps/api Worker
data-bc-product-id number no reads window.BCData PDP product context
data-initial-auth-token string no null Pre-authenticated portal JWT — skips magic-link flow

Configuration

Theming

The element uses light DOM (shadow: 'none'). Style it with standard CSS:

bc-subscriptions-widget {
  display: block;
  margin-top: 1rem;
  font-family: inherit;
}

bc-subscriptions-widget .subs-plan-card {
  border: 1px solid var(--color-border);
  border-radius: 0.5rem;
}

Auth integration

The magic-link flow is self-contained — the widget sends the shopper's email, they receive a link, and clicking it sets data-initial-auth-token for subsequent page loads. If your headless storefront already has a customer session (e.g., BC GraphQL customer token), you can pass it directly:

const token = await fetchPortalToken(bcCustomerId); // your server-side logic
element.setAttribute('data-initial-auth-token', token);

Portal tokens are short-lived JWTs signed by the API Worker's JWT_SECRET.


Common pitfalls

1. Custom element registers but renders blank

Cause: data-store-hash or data-api-base-url is missing or incorrect. Fix: Open DevTools → Elements, inspect the <bc-subscriptions-widget> tag, and verify both attributes are present. Check the Network tab for failed calls to the API Worker.

2. customElements.define() throws "already defined"

Cause: The element is registered twice — once from the CDN IIFE and once from the npm import. Fix: Pick one delivery path (CDN or npm) per page. If using Next.js, ensure the dynamic import runs only once (wrap in useEffect with an empty dependency array).

3. SSR renders empty element, hydration mismatch

Cause: The custom element registration is client-only but the element tag is present in server-rendered HTML. Fix: The element renders blank during SSR and hydrates on the client — this is expected behavior. If hydration mismatches appear in React, render the element only after mount (move it inside the useEffect or use suppressHydrationWarning).

4. CORS blocked on data-api-base-url

Cause: The API Worker's allowed origins don't include your headless storefront domain. Fix: Set CORS_ORIGINS in the Worker's environment variables to include your storefront origin.

5. data-bc-product-id not picked up

Cause: On non-BC headless storefronts, window.BCData is absent and the attribute must be explicit. Fix: Always pass data-bc-product-id explicitly when not running on a BC storefront.


Next steps

  • SDK reference — for building fully custom UI on top of the API (bypassing the web component entirely)
  • Catalyst integration — if you're on Catalyst (Next.js App Router with BC GraphQL)
  • ADR-0013 — component-shape rationale