> ## Documentation Index
> Fetch the complete documentation index at: https://dev.smile.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Shopify Hydrogen

> Add Smile to a Shopify Hydrogen storefront using the JavaScript SDK or Smile UI.

Yes, Smile works on [Shopify Hydrogen](https://shopify.dev/docs/storefronts/headless/hydrogen)! Because a Hydrogen storefront is a custom React app backed by Shopify, the same approach used for any [headless or single page application](/guides/use-cases/custom-frontend/headless) applies. You can use the [JavaScript SDK](/js/introduction) to build a fully custom loyalty experience, or drop in [Smile UI](/ui/introduction) for a pre-built panel and launcher.

This guide covers the three Hydrogen-specific things you'll need to handle: generating the customer token in a server loader, allowlisting Smile in your Content Security Policy, and accounting for Hydrogen's client-side navigation.

<Note>
  Hydrogen is a React Router app deployed to Shopify's Oxygen edge runtime (a Workers-style runtime, not Node.js). This guide was validated against Hydrogen 2026.4 and React Router 7. The exact APIs may differ slightly between Hydrogen versions — check Shopify's documentation if something doesn't match your setup.
</Note>

## Choose how to integrate

* **JavaScript SDK (recommended for Hydrogen)** — Build a fully custom loyalty UI as React components. Because the JavaScript SDK refreshes customer state via its own methods, it works naturally with Hydrogen's client-side navigation and doesn't require full page reloads.
* **Smile UI** — Add Smile's pre-built panel and launcher with minimal code. Works on Hydrogen, but the panel relies on full page reloads to refresh customer state, so you'll need to trigger reloads manually (see below).
* **Both** — Use Smile UI for the panel and the JavaScript SDK for custom flows by passing `includeSdk: true` when [initializing Smile UI](/ui/initializing).

## Required setup

Regardless of which integration approach you choose, you'll need to complete the following required setup.

### 1. Define your environment variables

The rest of this guide assumes two environment variables are available to your loaders through `context.env`:

* `SMILE_PUBLISHABLE_KEY` — identifies your Smile account and is safe to expose in client-side code. The help docs cover [where to find your publishable key](https://help.smile.io/en/articles/11878403-find-publishable-key).
* `SMILE_SIGNING_KEY` — signs the customer tokens you'll generate in the next step. The help docs cover [how to create a signing key](https://help.smile.io/en/articles/11878129-manage-signing-keys).

Add both to your local `.env` file, and set them as [environment variables on your Hydrogen storefront](https://shopify.dev/docs/storefronts/headless/hydrogen/environments) so they're available to deployed environments.

<Warning>
  The signing key is a secret. Keep it in environment variables — never include it in client-side code or commit it to your repository.
</Warning>

### 2. Generate the customer token

Smile uses [customer tokens](/js/concepts/customer-tokens) to identify the currently logged-in user, and they must be generated on the backend to ensure your Smile signing key is never exposed. In Hydrogen, loaders and actions run server-side, which is the right place to generate the customer token.

Generating a customer token involves two steps: using the [Customer Account API](https://shopify.dev/docs/storefronts/headless/building-with-the-customer-account-api/hydrogen) to identify the logged-in customer, then signing a JWT that contains the numeric portion of their Shopify customer ID.

<Steps>
  <Step title="Get the logged-in customer's ID">
    ```js app/root.jsx theme={null}
    export async function loader({ context }) {
      let customerToken = null;

      if (await context.customerAccount.isLoggedIn()) {
        const { data } = await context.customerAccount.query(`#graphql
          query { customer { id } }
        `);

        // data.customer.id is a GID, e.g. "gid://shopify/Customer/10733458"
        const shopifyCustomerId = data.customer.id.split('/').pop();
        customerToken = await generateSmileCustomerToken(shopifyCustomerId, context.env);
      }

      return {
        customerToken,
        publishableKey: context.env.SMILE_PUBLISHABLE_KEY,
        // ...the rest of your existing root loader data
      };
    }
    ```
  </Step>

  <Step title="Generate a signed JWT">
    The Node `jsonwebtoken` library isn't available on Oxygen, so you must use a runtime-compatible JWT library such as [`jose`](https://github.com/panva/jose) (which uses Web Crypto), or Web Crypto's `crypto.subtle` directly:

    ```js app/lib/smile.server.js theme={null}
    import { SignJWT } from 'jose';

    export async function generateSmileCustomerToken(shopifyCustomerId, env) {
      const secret = new TextEncoder().encode(env.SMILE_SIGNING_KEY);

      return await new SignJWT({})
        .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
        .setAudience('api.smile.io')
        .setSubject(`ShopifyCustomer:${shopifyCustomerId}`)
        .setExpirationTime('1h')
        .sign(secret);
    }
    ```

    Things to consider:

    * The `sub` must be the **numeric Shopify customer ID** (the tail of the `gid://shopify/Customer/…` GID), not Smile's internal customer ID — the two look alike, but a token signed with the wrong one is rejected.
    * Choose an expiry that covers a full browsing session. Smile verifies the token's expiry on every API call, and your root loader only generates a fresh token on a full page load — a short-lived token causes loyalty requests to start failing for customers who keep a page open longer than the expiry. One hour matches the lifetime Shopify uses for its own [Customer Account API access tokens](https://shopify.dev/docs/api/customer/latest).
  </Step>
</Steps>

### 3. Allowlist Smile in your CSP

Hydrogen applies a strict [Content Security Policy](https://shopify.dev/docs/storefronts/headless/hydrogen/content-security-policy) by default, which will block Smile until you allowlist its origins. Extend the policy in `entry.server.jsx`, picking the tab that matches how you chose to integrate:

<Tabs>
  <Tab title="JavaScript SDK">
    ```js app/entry.server.jsx theme={null}
    const { nonce, header, NonceProvider } = createContentSecurityPolicy({
      shop: {
        checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
        storeDomain: context.env.PUBLIC_STORE_DOMAIN,
      },
      scriptSrc: ["'self'", 'https://cdn.shopify.com', 'https://sdk.smile.io'],
      connectSrc: ['https://sdk.smile.io', 'https://api.smile.io'],
    });
    ```
  </Tab>

  <Tab title="Smile UI">
    ```js app/entry.server.jsx theme={null}
    const { nonce, header, NonceProvider } = createContentSecurityPolicy({
      shop: {
        checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
        storeDomain: context.env.PUBLIC_STORE_DOMAIN,
      },
      scriptSrc: ["'self'", 'https://cdn.shopify.com', 'https://js.smile.io'],
      connectSrc: [
        'https://js.smile.io',
        'https://platform.smile.io', // Smile UI API — note: NOT api.smile.io
        'https://auth.smile.io',
      ],
      styleSrc: ['https://js.smile.io'],
      imgSrc: [
        "'self'",
        'https://cdn.shopify.com',
        'https://shopify.com',
        'https://*.smile.io',        // launcher icons
        'data:',
      ],
      frameSrc: ["'self'", 'https://js.smile.io'], // panel/launcher iframes
    });
    ```

    **Smile UI does not use `api.smile.io`** — it calls `platform.smile.io` and `auth.smile.io`. If these are missing, Smile UI fails *silently*: no launcher, no visible CSP error, and `SmileUI.ready()` rejects with an internal error.

    Programs with custom reward images may serve assets from other hosts. Watch the console and extend `imgSrc` if launcher or panel images fail to load.
  </Tab>

  <Tab title="Both">
    ```js app/entry.server.jsx theme={null}
    const { nonce, header, NonceProvider } = createContentSecurityPolicy({
      shop: {
        checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
        storeDomain: context.env.PUBLIC_STORE_DOMAIN,
      },
      scriptSrc: [
        "'self'",
        'https://cdn.shopify.com',
        'https://sdk.smile.io',   // JavaScript SDK
        'https://js.smile.io',    // Smile UI
      ],
      connectSrc: [
        'https://api.smile.io',      // JavaScript SDK API
        'https://sdk.smile.io',
        'https://js.smile.io',
        'https://platform.smile.io', // Smile UI API — note: NOT api.smile.io
        'https://auth.smile.io',
      ],
      styleSrc: ['https://js.smile.io'],
      imgSrc: [
        "'self'",
        'https://cdn.shopify.com',
        'https://shopify.com',
        'https://*.smile.io',        // Smile UI launcher icons
        'data:',
      ],
      frameSrc: ["'self'", 'https://js.smile.io'], // Smile UI panel/launcher iframes
    });
    ```

    **Smile UI does not use `api.smile.io`** — it calls `platform.smile.io` and `auth.smile.io`. If these are missing, Smile UI fails *silently*: no launcher, no visible CSP error, and `SmileUI.ready()` rejects with an internal error.

    Programs with custom reward images may serve assets from other hosts. Watch the console and extend `imgSrc` if launcher or panel images fail to load.
  </Tab>
</Tabs>

<Warning>
  In every directive Hydrogen [doesn't set a default value for](https://github.com/Shopify/hydrogen/blob/main/packages/hydrogen/src/csp/csp.ts) (`scriptSrc`, `imgSrc`, and `frameSrc` above), re-add `'self'` and `https://cdn.shopify.com` (and `https://shopify.com` for images) yourself. Once a directive is defined, the browser [no longer falls back to `default-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/default-src), so omitting them blocks your own scripts and product images. Hydrogen merges your values into the directives it does set defaults for (`connectSrc`, `styleSrc`), so those only need Smile's origins.
</Warning>

## Using the JavaScript SDK

This is the recommended approach for Hydrogen, since it gives you complete control to create a fully custom React-based loyalty UI and doesn't rely on full page reloads.

<Steps>
  <Step title="Include the JavaScript SDK">
    Load Smile's JavaScript SDK with Hydrogen's [`Script` component](https://shopify.dev/docs/api/hydrogen/latest/components/script), which automatically applies the CSP nonce. Always load the JavaScript SDK from Smile's CDN rather than bundling it.

    ```jsx app/root.jsx theme={null}
    import { Script } from '@shopify/hydrogen';
    // ...inside <body>
    <Script src="https://sdk.smile.io/smile-js/v1/smile.js" />
    ```

    <Warning>
      Don't pass `waitForHydration` to the `Script` component. It injects the script while React is still hydrating, which fails hydration on every page load — and the JavaScript SDK never loads. The plain `<Script>` component renders the script tag with the CSP nonce server-side and hydrates cleanly.
    </Warning>
  </Step>

  <Step title="Initialize the JavaScript SDK">
    Create a new component file that loads and initializes the JavaScript SDK. It should wait for the [`smile-js-loaded` event](/js/concepts/loaded-event) then [initialize](/js/smile/initialize) with your publishable key, the customer token from your loader, and any [resources to preload](/js/concepts/preloading).

    ```jsx app/components/SmileSdk.jsx theme={null}
    import { useEffect, useRef } from 'react';
    import { useRouteLoaderData } from 'react-router';

    export function SmileSdk() {
      const { publishableKey, customerToken } = useRouteLoaderData('root');
      const initialized = useRef(false);

      useEffect(() => {
        const init = () => {
          if (initialized.current) return;
          initialized.current = true;

          window.Smile?.initialize({
            publishableKey,
            customerToken,
            // Customer-specific resources can only be preloaded when a
            // customer is logged in — including them for a logged-out
            // visitor causes initialization to fail.
            preload: customerToken
              ? ['pointsSettings', 'customerPointsProducts']
              : ['pointsSettings'],
          }).catch((error) => console.error('Smile failed to initialize:', error));
        };

        if (window.Smile) init();
        else document.addEventListener('smile-js-loaded', init, { once: true });
      }, [publishableKey, customerToken]);

      return null;
    }
    ```

    The `initialized` ref guards initialization to run exactly once per page load — React's `StrictMode` runs effects twice in development, and this effect re-runs whenever the customer token changes. Session changes after initialization are handled with dedicated methods instead, covered in the login and logout step below.

    Render `<SmileSdk />` once in your root layout, alongside the `Script` tag, so the JavaScript SDK initializes on every route:

    ```jsx app/root.jsx theme={null}
    import { Script } from '@shopify/hydrogen';
    import { SmileSdk } from '~/components/SmileSdk'; // [!code ++]

    // ...inside <body>
    <Script src="https://sdk.smile.io/smile-js/v1/smile.js" />
    <SmileSdk /> // [!code ++]
    ```

    <Note>
      On Remix-based Hydrogen versions (2025.1 and earlier), import `useRouteLoaderData` from `@remix-run/react` instead — React Router 7 [replaced the Remix packages](https://reactrouter.com/upgrading/remix).
    </Note>
  </Step>

  <Step title="Handle login and logout">
    Hydrogen's default [Customer Account API login](https://shopify.dev/docs/storefronts/headless/building-with-the-customer-account-api/hydrogen) redirects through Shopify's hosted login page — a full navigation — so when the customer lands back on your storefront, your root loader generates a customer token for the new session and Smile's JavaScript SDK initializes with the logged-in customer automatically. No extra work needed.

    If your app changes the customer session without a full navigation, tell the JavaScript SDK directly. The customer token comes from the same place: after your root loader revalidates, read the updated `customerToken` from `useRouteLoaderData('root')` and pass it along:

    ```js JavaScript theme={null}
    // After login — pass the fresh token from your root loader
    await Smile.customer.login({ customerToken, preload: ['rewardFulfillments'] });

    // After logout
    await Smile.customer.logout();
    ```

    After any action that changes a customer's points balance or rewards information, call [`Smile.preload()`](/js/smile/preload) to pull in the latest data. When calling this method, make sure you pass in the appropriate [resource keys](/js/concepts/preloading#resources-that-can-be-preloaded) for the data you want refreshed.
  </Step>

  <Step title="Build your loyalty UI">
    Use the [available JavaScript SDK methods](/js/introduction) to implement key loyalty flows. At a minimum, Smile recommends:

    * A list of the available ways to earn points
    * A place for customers to see their points balance, VIP tier, and unused rewards
    * A way for customers to redeem their points at checkout
  </Step>
</Steps>

## Using Smile UI

Use Smile UI if you want loyalty information displayed to customers in Smile's pre-built panel and launcher, with minimal custom code required.

### Key considerations

* **Page reloads** — The panel and launcher rely on full page reloads to detect login/logout and refresh customer state. Because Hydrogen navigates client-side, you must manually trigger a full reload (`window.location.reload()`) whenever a customer logs in or out, or after any action that changes their points balance or rewards information (like redeeming for a coupon). Calling `SmileUI.initialize()` again without a full reload is not sufficient.
* **Nudges** — Nudges are not triggered or visible when Smile UI is added to a Hydrogen storefront.
* **Translations** — On a Hydrogen storefront, the panel will not auto-detect the customer's browser language. Instead, loyalty content will always be presented in the language configured for the program in Smile Admin.

### Integration instructions

<Steps>
  <Step title="Include Smile UI from the CDN">
    ```jsx app/root.jsx theme={null}
    import { Script } from '@shopify/hydrogen';
    // ...inside <body>
    <Script src="https://js.smile.io/v1/smile-ui.js" />
    ```

    <Warning>
      Don't pass `waitForHydration` to the `Script` component. It injects the script while React is still hydrating, which fails hydration on every page load — and Smile UI never loads. The plain `<Script>` component renders the script tag with the CSP nonce server-side and hydrates cleanly.
    </Warning>
  </Step>

  <Step title="Initialize Smile UI">
    Create a new component file that [initializes](/ui/initializing) Smile UI with your publishable key and the customer token from your loader. Pass `includeSdk: true` if you also want the JavaScript SDK available.

    Because the script loads before React hydrates, `SmileUI` is usually already available by the time your effect runs — check for `window.SmileUI` first and fall back to the [`smile-ui-loaded` event](/ui/events/ui-loaded). Listening for the event alone isn't enough, since it fires before any effect runs.

    ```jsx app/components/SmileUi.jsx theme={null}
    import { useEffect, useRef } from 'react';
    import { useRouteLoaderData } from 'react-router';

    export function SmileUi() {
      const { publishableKey, customerToken } = useRouteLoaderData('root');
      const initialized = useRef(false);

      useEffect(() => {
        const init = () => {
          if (initialized.current) return;
          initialized.current = true;

          window.SmileUI.initialize({ publishableKey, customerToken });
          window.SmileUI.ready().catch((error) =>
            console.error('Smile UI failed to initialize:', error)
          );
        };

        if (window.SmileUI) init();
        else document.addEventListener('smile-ui-loaded', init, { once: true });
      }, [publishableKey, customerToken]);

      return null;
    }
    ```

    The `initialized` ref in this example guards initialization to run exactly once per page load. If `SmileUI.initialize()` runs a second time — React's `StrictMode` runs effects twice in development, and this effect re-runs whenever the customer token changes — [`SmileUI.openPanel()`](/ui/panel/open) silently stops working. Reflect session changes with a full page reload instead of re-initializing.

    Unlike the JavaScript SDK's `Smile.initialize()`, `SmileUI.initialize()` does not return a Promise, so chaining `.then()` on it throws. `SmileUI.ready()` is the Promise-based completion signal.

    Render `<SmileUi />` once in your root layout, alongside the `Script` tag:

    ```jsx app/root.jsx theme={null}
    import { Script } from '@shopify/hydrogen';
    import { SmileUi } from '~/components/SmileUi'; // [!code ++]

    // ...inside <body>
    <Script src="https://js.smile.io/v1/smile-ui.js" />
    <SmileUi /> // [!code ++]
    ```
  </Step>

  <Step title="Trigger full reloads after key customer actions">
    Ensure a full page reload occurs whenever a customer logs in or out, or when an action outside the panel changes their points balance or rewards information. You can call `window.location.reload()` or an equivalent for your setup.

    If you use Hydrogen's default [Customer Account API login](https://shopify.dev/docs/storefronts/headless/building-with-the-customer-account-api/hydrogen), login and logout already happen as full page loads — Shopify's hosted login page redirects back to your storefront — so Smile UI picks up the new session automatically. You only need to trigger reloads for actions that change customer state without a full navigation. Logged-out visitors have no customer state to refresh, so reloads only matter once a customer is logged in.
  </Step>

  <Step title="Add links to open the panel">
    Add menu items or buttons that open the panel to a specific screen using [deep links](/ui/panel/deep-links).

    Because Hydrogen navigates client-side, the `smile_deep_link` query parameter and `#smile-…` anchor mechanisms only take effect on a full page load — they never fire when a customer navigates with React Router's `<Link>` component.

    * To open the panel on the page the customer is already on, use the `data-smile-deep-link` HTML attribute — it works on any anchor, wherever it's rendered.
    * To navigate to a different page and then automatically open the panel, force a full navigation with `<Link reloadDocument>` or a plain `<a>` tag.

    ```jsx JSX theme={null}
    {/* Opens the panel on the current page */}
    <a href="#" data-smile-deep-link="points_products">Redeem your points</a>

    {/* Navigates to /rewards, then opens the panel there */}
    <Link to="/rewards?smile_deep_link=points_products" reloadDocument>
      Rewards
    </Link>
    ```
  </Step>
</Steps>
