Shopify Headless

Integrating VWO FE SDK with Shopify Headless (Hydrogen + Oxygen)


Overview

Shopify Headless with Hydrogen + Oxygen is a modern, server-centric approach where Hydrogen (React + Remix) renders pages, and Oxygen (Shopify’s Edge environment) executes server-side code.

To run experiments and feature flags reliably in this architecture, we need a client that can:

  • Execute on the **server **(Oxygen)
  • Hydrate in the browser
  • Run at the Edge if available

The VWO FE SDK — available at GitHub — supports Node.js, browser, and Edge, making it ideal.


Architectural Considerations

LayerExecution EnvironmentHydration
Remix LoadersServer / Oxygen (Edge)N/A
React Rendering & HydrationBrowserYes
VWO Tracking ScriptsBrowserYes
Variant AssignmentServer / Browser / EdgeVaries

Key Design Principle: Always determine feature flags and experiment variations before sending the HTML. This avoids flickering and inconsistent UX.


Why VWO FE SDK for Headless Shopify

  • Multi-environment compatibility: Node, Browser, Edge
  • Unified API for feature flags & experiments
  • Works well with SSR frameworks like Remix
  • Ideal for early variant selection during SSR

Environment Constraints (Oxygen & Edge)

Oxygen is an Edge-like environment with:

  • No Node’s native modules (fs, net, etc.)
  • Global fetch API
  • Fast cold starts
  • Streaming SSR

This means:

  • ✔ SDK must support Edge execution
  • ❌ Cannot rely on Node-only modules

Hydrogen + Oxygen Compatibility

EnvironmentCan run VWO FE SDK?Notes
Node.js Server (SSR)Works normally
Oxygen Edge⚠️Must await async tracking
Browser JS⚠️SDK key exposed (use read-only keys)

VWO’s FE SDK is Edge compatible, so we can initialize it server-side during Remix loader execution.

Step-by-Step Integration

Prerequisites

  • A Shopify Hydrogen project (Remix routing)
  • VWO Account and SDK Key (for the specific environment)
  • Unique user identifiers (for consistent assignments)

Installing the SDK

npm install vwo-fme-node-sdk --save
# Or:
yarn add vwo-fme-node-sdk

This installs the SDK, which will run in both server and browser contexts.

Initializing the SDK (Server / Oxygen)

Store keys as environment variables:

VWO_SDK_SERVER_KEY=your_server_key
VWO_SDK_CLIENT_KEY=your_browser_key

Server-Side Integration (Hydrogen Loader)

Hydrogen uses Remix loaders to fetch data before rendering. In this step:

  • We initialize VWO FE SDK in the Hydrogen loader
  • We fetch experiment assignment
  • We return data for the React UI

Goal: Evaluate flags & experiment variants in the loader so Hydrogen can render page accordingly.

Create a helper file like vwo.server.js:

import { init } from "vwo-fme-node-sdk";
export async function createVWOClient() {
  const vwoClient = await init({
    accountId: process.env.VWO_ACCOUNT_ID,
    sdkKey: process.env.VWO_SDK_KEY,
  });
  return vwoClient;
}

Inside a Remix route’s loader: Example: app/routes/index.server.ts

import { createVWOClient } from "~/vwo.server";

export async function loader({ request }) {
  const vwoClient = await createVWOClient();
  const userContext = { id: "user_123" };
  const featureFlag = await vwoClient.getFlag("new_checkout", userContext);



  const isEnabled = featureFlag.isEnabled();
  const variableValue = featureFlag.getVariable("rollout_percentage", "default");

  return { isEnabled, variableValue };
}

This evaluates feature flags before rendering the page.


Client-Side Integration (Browser)

Even though assignments are computed server-side, we may want to trigger:

  • Event tracking
  • Conversion tracking
  • Client-only experiments

Hydrate flags in your React component:

import { useLoaderData } from "@remix-run/react";

export default function Home() {
  const { experimentAssignment } = useLoaderData();

  return (
    <div>
      <h1>Welcome</h1>
      {experimentAssignment.variation === "variant1" && (
        <div>Variant 1 UI</div>
      )}
    </div>
  );
}

After server rendering, you can still track events from the UI:

import { init } from "vwo-fme-node-sdk";

if (typeof window !== "undefined") {
  (async function () {
    const vwoClient = await init({
      accountId: process.env.VWO_ACCOUNT_ID,
      sdkKey: process.env.VWO_SDK_BROWSER_KEY,
    });

    vwoClient.trackEvent("add_to_cart", {
      id: "user_123",
    });
  })();
}

Use client SDK keys for browser embeds, which are read-only and safe (but visible).

📘

Important notes:

  • Use browser SDK for event tracking
  • Use server SDK for assignment / experiment variant selection

Edge Usage (Oxygen)

Oxygen behaves like Edge (Cloudflare Workers). The FE SDK works there because:

  • It uses the browser-fetch API
  • Doesn’t rely on Node native modules

Edge runtimes like Oxygen may terminate async work early.

To ensure tracking completes, you must provide edgeConfig in init options and call flushEvents using waitUntil.

import { init } from "vwo-fme-node-sdk";

// CRITICAL: provide edgeConfig in init options 
export async function createVWOClient() {
  const vwoClient = await init({
    accountId: process.env.VWO_ACCOUNT_ID,
    sdkKey: process.env.VWO_SDK_KEY,
    edgeConfig: {
	shouldWaitForTrackingCalls: true,
    }
  });
  return vwoClient;
}

export async function loader({context, request}: LoaderFunctionArgs) {
  const vwoClient = await createVWOClient();

  await vwoClient.trackEvent("checkout_completed", { id: "user_123" });

  // CRITICAL: Flush events using waitUntil API
  context.waitUntil(vwoClient.flushEvents());

  return new Response("OK");
}

🚧

⚠️ Required Vite configuration (Build-time) When using Vite-based frameworks (Oxygen), you must also update your vite.config.ts.


Why is this needed

Vite treats SSR dependencies differently from browser dependencies. The vwo-fme-node-sdk has deep internal imports that Vite does not automatically pre-bundle for SSR.

In Edge environments like Oxygen, this can lead to:

  • Runtime module resolution errors
  • CJS ↔ ESM incompatibilities
  • “Module not found” errors during SSR execution

To avoid this, you must explicitly tell Vite to pre-bundle the vwo-fme-node-sdk and its internal dependencies for SSR.


Add the following to your vite.config.ts:

export default defineConfig({
  ssr: {
    optimizeDeps: {
      include: [
        "vwo-fme-node-sdk",
        "murmurhash",
        "vwo-fme-sdk-log-messages",
      ],
    },
  },
});
🚧

Do NOT let un-awaited asynchronous calls hang — they might get dropped. Read more on Edge optimizations here - https://developers.vwo.com/v2/docs/fme-edge-support


Asynchronous Data Fetching & Fallbacks

Experiments happen during SSR, so:

  • Wrap SDK calls in error boundaries
  • Provide fallback behavior if the SDK fails
  • Use cached assignments if reused frequently

Example:

let assignment;
const defaultBehaviorValue;
try {
  const flag = await vwo.getFlag(key, userContext)
  assignment = flag.getVariable(, defaultBehaviorValue);
} catch (err) {
  console.error("VWO assignment failed:", err);
  assignment = defaultBehaviorValue;
}

Best Practices & Design Patterns

  • Use environment-specific SDK keys for dev/staging/production.
  • Evaluate features server-side for SSR decisions.
  • Hydrate assignments to the client for UI logic.
  • Persist consistent user IDs (cookies/localStorage).
  • Centralise SDK initialisation (avoid per-component initialisation).

❗ SDK Key Exposure — In browsers, SDK keys are public but read-only. Avoid putting sensitive logic based on them. ❗ Edge Execution Constraints — Must await tracking/event calls explicitly or risk drop. ❗ No built-in caching — The default client fetches on every init. Consider custom caching/storage for Edge if you have high load.


Should I run SDK initialization on every request?

Not necessarily — and you shouldn’t if you can avoid it.

VWO FE SDKs support initialising the SDK using already-fetched or cached settings. When you pass pre-fetched settings during initialization:

  • ❌ No network call is required during init
  • ⚡ Decisioning happens immediately and synchronously
  • ✅ Ideal for Edge-like environments (e.g., Shopify Oxygen)

This makes the SDK highly suitable for high-performance SSR and Edge runtimes, where repeated network calls during request handling should be avoided.


✅ Recommended approach (especially for Edge / Oxygen)

  • Fetch VWO settings periodically (for example, via a scheduled job, cache refresh, or shared storage).
  • Initialize the SDK using those cached settings.
  • Perform real-time feature flag and experiment evaluation without waiting on remote calls.

This ensures:

  • Consistent and fast decisioning
  • Minimal latency
  • Better scalability at the Edge
📘

You must ensure that cached settings are refreshed periodically so your application receives the latest changes made in the VWO application (new flags, updated rules, traffic changes, etc.).


Conclusion

Integrating VWO FE SDK with Shopify Headless (Hydrogen + Oxygen) gives you:

  • Consistent SSR feature flag evaluation
  • Fast UX with server-rendered variant decisions
  • Browser tracking & personalization
  • Multi-environment compatibility

By aligning with the official SDK usage and adapting to Remix/Oxygen constraints, you get a robust experimentation layer for your storefront.