# Flagship + Shopify Hydrogen Integration

> 📘 Github Repository
>
> <https://github.com/flagship-io/flagship-shopify-hydrogen-example>

### Overview

This guide demonstrates how to:

* Integrate Flagship feature flags with Shopify Hydrogen (React-based framework)
* Initialize the Flagship SDK with edge bucketing for optimal performance
* Create visitor objects with contextual data in the app load context
* Use feature flags in React components and loader functions
* Handle server-side rendering with Flagship
* Implement client-side hydration of feature flags
* Conditionally display content based on feature flags

### Prerequisites

* [Node.js](https://nodejs.org/)
* [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/)
* A [Shopify store](https://www.shopify.com/)
* A [Flagship account](https://app.flagship.io/login) with API credentials

### Setup

1. Clone the example repository:

```bash
git clone https://github.com/flagship-io/flagship-hydrogen-example
cd flagship-hydrogen-example
```

2. Install dependencies:

```bash
npm install
# or
yarn install
```

3. Configure environment variables:

Create a .env file with your Flagship credentials:

```bash
VITE_ENV_ID=your_flagship_environment_id
VITE_API_KEY=your_flagship_api_key
```

4. Start the development server:

```bash
npm run dev
# or
yarn dev
```

### Configure Vite for Flagship SDK

When using Flagship SDK with Hydrogen, proper Vite configuration is essential to prevent bundling issues. Update your vite.config.ts file with the following settings:

```typescript
export default defineConfig({
  // ...other configuration
  optimizeDeps: {
    exclude: [
      '@flagship.io/react-sdk',
      '@flagship.io/react-sdk/edge'
    ],
  },
  ssr: {
    optimizeDeps: {
      include: [],
      exclude: [],
    },
  },
});
```

This configuration:

* Excludes both the main and edge bundles from client-side optimization
* Prevents Vite from processing the SDK in ways that might break its functionality

> ⚠️ **Important**: In Hydrogen (and other edge/SSR environments), always import from the edge bundle:
>
> ```tsx
> // Correct import for Hydrogen
> import { ... } from '@flagship.io/react-sdk/edge';
> ```

### Initialize Flagship SDK in Hydrogen

The Flagship SDK is initialized at the application level to ensure it's available throughout your Hydrogen store. This is done in the flagship.ts helper file:

```typescript
// app/helpers/flagship.ts
import {
  Flagship,
  FSSdkStatus,
  DecisionMode,
  LogLevel,
  type NewVisitor,
} from '@flagship.io/react-sdk/edge';
import initialBucketing from './bucketing.json';

// Function to start the Flagship SDK
export async function startFlagshipSDK() {
  if (
    Flagship.getStatus() &&
    Flagship.getStatus() !== FSSdkStatus.SDK_NOT_INITIALIZED
  ) {
    return Flagship; // If it has been initialized, return early
  }
  return await Flagship.start(
    import.meta.env.VITE_ENV_ID,
    import.meta.env.VITE_API_KEY,
    {
      logLevel: LogLevel.DEBUG, // Set the log level
      fetchNow: false, // Do not fetch flags immediately
      decisionMode: DecisionMode.BUCKETING_EDGE, // set decision mode
      nextFetchConfig: {revalidate: 15}, // Set cache revalidation for SDK routes to 15 seconds
      initialBucketing, // Set initial bucketing data
    },
  );
}

// Helper function to create and fetch visitor data
export async function getFsVisitorData(visitorData: NewVisitor) {
  // Start the SDK in Bucketing Edge mode and get the Flagship instance
  const flagship = await startFlagshipSDK();

  // Create a visitor
  const visitor = flagship.newVisitor(visitorData);

  // Fetch flag values for the visitor
  await visitor.fetchFlags();

  // Return visitor instance
  return visitor;
}
```

The SDK is configured with:

* **Edge Bucketing Mode**: Makes flag decisions at the edge without API calls
* **Initial Bucketing Data**: Pre-loaded campaign data for local decision-making
* **Revalidation Config**: Refreshes cached flags every 15 seconds
* **Debug Logging**: Helps troubleshoot during development

### Create a Visitor in App Load Context

In Hydrogen, the Flagship visitor should be created in the `createAppLoadContext` function. This ensures the visitor is initialized once per request and available throughout the entire application context, including all loaders and actions.

```typescript
// In app/lib/context.ts
import {createHydrogenContext} from '@shopify/hydrogen';
import {AppSession} from '~/lib/session';
import {CART_QUERY_FRAGMENT} from '~/lib/fragments';
import {getFsVisitorData} from '~/helpers/flagship';

/**
 * The context implementation is separate from server.ts
 * so that type can be extracted for AppLoadContext
 */
export async function createAppLoadContext(
  request: Request,
  env: Env,
  executionContext: ExecutionContext,
) {
  /**
   * Open a cache instance in the worker and a custom session instance.
   */
  if (!env?.SESSION_SECRET) {
    throw new Error('SESSION_SECRET environment variable is not set');
  }

  const waitUntil = executionContext.waitUntil.bind(executionContext);
  const [cache, session] = await Promise.all([
    caches.open('hydrogen'),
    AppSession.init(request, [env.SESSION_SECRET]),
  ]);

  const hydrogenContext = createHydrogenContext({
    env,
    request,
    cache,
    waitUntil,
    session,
    i18n: {language: 'EN', country: 'US'},
    cart: {
      queryFragment: CART_QUERY_FRAGMENT,
    },
  });

  // Initialize Flagship visitor
  // Extract visitor ID from session or generate new one
  const visitorId = session.get('visitorId') || crypto.randomUUID();
  if (!session.has('visitorId')) {
    session.set('visitorId', visitorId);
  }

  const fsVisitorData = {
    visitorId,
    context: {
      // Add any context from request/session for targeting
      userAgent: request.headers.get('user-agent') as string,
      // You can add more context like:
      // country: hydrogenContext.storefront.i18n.country,
      // language: hydrogenContext.storefront.i18n.language,
    },
    hasConsented: true, // Get from cookie/session in production
  };

  // Fetch the Flagship visitor data
  // This will start the Flagship SDK and fetch the flags for the visitor
  const fsVisitor = await getFsVisitorData(fsVisitorData);

  return {
    ...hydrogenContext,
    fsVisitor, // Now available in all loaders/actions via context.fsVisitor
  };
}
```

#### Key Benefits of This Approach

* **Single Initialization**: The visitor is created once per request, not in every loader
* **Global Availability**: All loaders and actions can access `context.fsVisitor`
* **Session Integration**: Visitor ID persists across requests using Hydrogen's session
* **Request Context**: Can use request headers and other data for targeting
* **Performance**: Flags are fetched once and reused throughout the request lifecycle
* **Type Safety**: The visitor is available in the TypeScript context type

#### Accessing Visitor Data in Root Loader

The root loader serializes the visitor data for client-side hydration:

```typescript
// In app/root.tsx
import {Analytics, getShopAnalytics, useNonce} from '@shopify/hydrogen';
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {
  Outlet,
  useRouteError,
  isRouteErrorResponse,
  type ShouldRevalidateFunction,
  Links,
  Meta,
  Scripts,
  ScrollRestoration,
  useRouteLoaderData,
} from 'react-router';

export type RootLoader = typeof loader;

export async function loader(args: LoaderFunctionArgs) {
  // Start fetching non-critical data without blocking time to first byte
  const deferredData = loadDeferredData(args);

  // Await the critical data required to render initial state of the page
  const criticalData = await loadCriticalData(args);

  const {storefront, env, fsVisitor} = args.context;

  return {
    ...deferredData,
    ...criticalData,
    // Serialize visitor data for client-side hydration
    fsVisitorData: {
      visitorId: fsVisitor.visitorId,
      context: fsVisitor.context,
      hasConsented: fsVisitor.hasConsented,
      initialFlags: fsVisitor.getFlags().toJSON(),
    },
    publicStoreDomain: env.PUBLIC_STORE_DOMAIN,
    shop: getShopAnalytics({
      storefront,
      publicStorefrontId: env.PUBLIC_STOREFRONT_ID,
    }),
    consent: {
      checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN,
      storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN,
      withPrivacyBanner: false,
      country: args.context.storefront.i18n.country,
      language: args.context.storefront.i18n.language,
    },
  };
}
```

### Provide Flagship Context to Your Application

To make Flagship available throughout your application, use the `FsProvider` component to wrap your application:

```typescript
// In app/helpers/FsProvider.tsx
import {
  FlagshipProvider,
  LogLevel,
  SerializedFlagMetadata,
  VisitorData,
} from '@flagship.io/react-sdk/edge';

export function FsProvider({
  children,
  initialFlagsData,
  visitorData,
}: {
  children: React.ReactNode;
  initialFlagsData?: SerializedFlagMetadata[];
  visitorData?: VisitorData;
}) {
  return (
    <FlagshipProvider
      envId={import.meta.env.VITE_ENV_ID}
      apiKey={import.meta.env.VITE_API_KEY}
      logLevel={LogLevel.DEBUG}
      initialFlagsData={initialFlagsData}
      visitorData={visitorData || null}
    >
      {children}
    </FlagshipProvider>
  );
}
```

In the root Layout component, use this provider with the data from the loader:

```tsx
// In app/root.tsx Layout component
export function Layout({children}: {children?: React.ReactNode}) {
  const nonce = useNonce();
  const data = useRouteLoaderData<RootLoader>('root');

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <link rel="stylesheet" href={tailwindCss}></link>
        <link rel="stylesheet" href={resetStyles}></link>
        <link rel="stylesheet" href={appStyles}></link>
        <Meta />
        <Links />
      </head>
      <body>
        <FsProvider
          visitorData={data?.fsVisitorData}
          initialFlagsData={data?.fsVisitorData.initialFlags}
        >
          {data ? (
            <Analytics.Provider
              cart={data.cart}
              shop={data.shop}
              consent={data.consent}
            >
              <PageLayout {...data}>{children}</PageLayout>
            </Analytics.Provider>
          ) : (
            children
          )}
        </FsProvider>
        <ScrollRestoration nonce={nonce} />
        <Scripts nonce={nonce} />
      </body>
    </html>
  );
}
```

### Use Feature Flags in React Components

Once Flagship is initialized, you can use feature flags in your components with the `useFsFlag` hook:

#### Example 1: Change Text Based on a Flag

```tsx
// In app/routes/_index.tsx RecommendedProducts component
import {useFsFlag} from '@flagship.io/react-sdk/edge';

function RecommendedProducts({
  products,
}: {
  products: Promise<RecommendedProductsQuery | null>;
}) {
  // Get the flag with a default value
  const headingFlag = useFsFlag('recommended_products_heading');
  
  return (
    <div className="recommended-products">
      <h2>{headingFlag.getValue('Recommended Products')}</h2>
      <Suspense fallback={<div>Loading...</div>}>
        <Await resolve={products}>
          {(response) => (
            <div className="recommended-products-grid">
              {response
                ? response.products.nodes.map((product) => (
                    <ProductItem key={product.id} product={product} />
                  ))
                : null}
            </div>
          )}
        </Await>
      </Suspense>
    </div>
  );
}
```

#### Example 2: Conditionally Display Content Based on a Flag

```tsx
// In app/components/ProductItem.tsx
import {useFsFlag} from '@flagship.io/react-sdk/edge';

export function ProductItem({
  product,
  loading,
}: {
  product:
    | CollectionItemFragment
    | ProductItemFragment
    | RecommendedProductFragment;
  loading?: 'eager' | 'lazy';
}) {
  // Get flag to control discount message visibility
  const discountFlag = useFsFlag('show_discount_message');
  const showDiscount = discountFlag.getValue(false);

  return (
    <Link
      className="product-item"
      key={product.id}
      prefetch="intent"
      to={variantUrl}
    >
      {product.featuredImage && (
        <Image
          alt={product.featuredImage.altText || product.title}
          aspectRatio="1/1"
          data={product.featuredImage}
          loading={loading}
          sizes="(min-width: 45em) 400px, 100vw"
        />
      )}
      <h4>{product.title}</h4>
      <small>
        <Money data={product.priceRange.minVariantPrice} />
      </small>
      
      {/* Conditionally render discount message based on flag */}
      {showDiscount && (
        <div className="discount-message">
          Special discount available!
        </div>
      )}
    </Link>
  );
}
```

### Use Feature Flags in Loader Functions

Since the Flagship visitor is now available in the app context, you can easily access feature flags in any loader function. This is powerful for server-side decision making before rendering.

#### Example 1: Control Product Count Based on a Flag

```typescript
// In app/routes/_index.tsx or any route loader
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';

export async function loader(args: LoaderFunctionArgs) {
  const {context} = args;
  
  // Access the visitor from context
  const {fsVisitor} = context;
  
  // Get flag value to control number of products fetched
  const productCountFlag = fsVisitor.getFlag('recommended_products_count');
  const productCount = productCountFlag.getValue(4); // Default to 4
  
  // Get flag to determine sorting strategy
  const sortingFlag = fsVisitor.getFlag('product_sorting_strategy');
  const sortBy = sortingFlag.getValue('UPDATED_AT');

  const deferredData = loadDeferredData(args);
  const criticalData = await loadCriticalData(args);

  return {
    ...deferredData,
    ...criticalData,
    productCount,
    sortBy,
  };
}

// Use the flag value in your component
export default function Homepage() {
  const data = useLoaderData<typeof loader>();
  return (
    <div className="home">
      <FeaturedCollection collection={data.featuredCollection} />
      <RecommendedProducts 
        products={data.recommendedProducts}
        count={data.productCount}
      />
    </div>
  );
}

function RecommendedProducts({
  products,
  count,
}: {
  products: Promise<RecommendedProductsQuery | null>;
  count?: number;
}) {
  const headingFlag = useFsFlag('recommended_products_heading');
  return (
    <div className="recommended-products">
      <h2>{headingFlag.getValue('Recommended Products')}</h2>
      <Suspense fallback={<div>Loading...</div>}>
        <Await resolve={products}>
          {(response) => (
            <div className="recommended-products-grid">
              {response
                ? response.products.nodes.slice(0, count || 4).map((product) => (
                    <ProductItem key={product.id} product={product} />
                  ))
                : null}
            </div>
          )}
        </Await>
      </Suspense>
    </div>
  );
}
```

#### Example 2: Conditional Data Loading

```typescript
// Load different data based on feature flags
export async function loader({context}: LoaderFunctionArgs) {
  const {storefront, fsVisitor} = context;

  // Check if personalized recommendations are enabled
  const personalizedFlag = fsVisitor.getFlag('enable_personalized_recommendations');
  const usePersonalized = personalizedFlag.getValue(false);

  let products;
  if (usePersonalized) {
    // Load personalized recommendations
    products = await loadPersonalizedProducts(context);
  } else {
    // Load standard product list
    products = await storefront.query(STANDARD_PRODUCTS_QUERY);
  }

  return {
    products,
    isPersonalized: usePersonalized,
  };
}
```

#### Benefits of Using Flags in Loaders

* **Server-side Decision Making**: Make feature decisions before page render, improving performance
* **Performance Optimization**: Control data fetching based on flags to reduce unnecessary queries
* **A/B Testing**: Test different data loading strategies and measure impact
* **Gradual Rollouts**: Enable features progressively for different user segments
* **Consistent Context**: Same visitor instance used across all loaders in a request
* **SEO Friendly**: Flags resolved server-side are immediately available for crawlers

#### Best Practices for Loader Flags

1. **Access via Context**: Always use `context.fsVisitor` rather than creating new visitors

```typescript
// ✅ Correct
export async function loader({context}: LoaderFunctionArgs) {
  const flag = context.fsVisitor.getFlag('my_flag');
}

// ❌ Incorrect - creates new visitor
export async function loader({context}: LoaderFunctionArgs) {
  const visitor = await getFsVisitorData({visitorId: 'test'});
  const flag = visitor.getFlag('my_flag');
}
```

2. **Default Values**: Always provide default values for flags to ensure graceful fallbacks

```typescript
// ✅ Always provide defaults
const productCount = fsVisitor.getFlag('products_per_page').getValue(10);

// ❌ No default could cause issues
const productCount = fsVisitor.getFlag('products_per_page').getValue();
```

### Send analytics data back to Flagship

To measure the impact of your feature flags, you need to send analytics data back to Flagship. Analytics can be sent from two places:

1. **From loaders** (Server-side) - Hits are pooled and sent in batch when `Flagship.close()` is called via `waitUntil`
2. **From components** (Client-side) - Hits are pooled and sent in batch automatically in the background by the React SDK

#### Send Analytics from Loaders (Server-Side)

In loaders, calling `sendHits` doesn't immediately send the hits to Flagship. Instead, hits are added to a pool and sent in batch when `Flagship.close()` is called in the background via `waitUntil`.

```typescript
// In app/lib/context.ts
import {createHydrogenContext} from '@shopify/hydrogen';
import {AppSession} from '~/lib/session';
import {CART_QUERY_FRAGMENT} from '~/lib/fragments';
import {getFsVisitorData} from '~/helpers/flagship';
import {Flagship} from '@flagship.io/react-sdk/edge';

export async function createAppLoadContext(
  request: Request,
  env: Env,
  executionContext: ExecutionContext,
) {
  // ...existing code...

  const waitUntil = executionContext.waitUntil.bind(executionContext);

  // ...existing code...

  // Fetch the Flagship visitor data
  const fsVisitor = await getFsVisitorData(fsVisitorData);

  // Ensure Flagship SDK is closed after response 
  // This will flush all pooled hits in the background
  waitUntil?.(Flagship.close());

  return {
    ...hydrogenContext,
    fsVisitor,
  };
}
```

**How it works (Server-Side):**

1. Call `fsVisitor.sendHits([...])` in any loader - hits are added to an internal pool
2. Return the response immediately (non-blocking)
3. `Flagship.close()` is called via `waitUntil` in the background
4. All pooled hits are sent to Flagship in batch before the worker terminates

**Example: Send analytics from route loaders**

```typescript
// In app/routes/_index.tsx
import { EventCategory, HitType } from "@flagship.io/react-sdk/edge";

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

  // Get flag values
  const newFeatureFlag = fsVisitor.getFlag('enable_new_checkout');
  const isEnabled = newFeatureFlag.getValue(false);

  // Add hits to the pool (non-blocking, no await needed but can be used)
  await fsVisitor.sendHits([
    {
      type: HitType.PAGE_VIEW,
      documentLocation: request.url,
    },
    {
      type: HitType.EVENT,
      category: EventCategory.ACTION_TRACKING,
      action: 'checkout_version_view',
      label: isEnabled ? 'new_checkout' : 'old_checkout',
      value: 1,
    },
  ]);

  // Hits will be sent when Flagship.close() is called in context.ts
  // Continue with data loading immediately

  const deferredData = loadDeferredData({context});
  const criticalData = await loadCriticalData({context});

  return {
    ...deferredData,
    ...criticalData,
    isEnabled,
  };
}
```

#### Send Analytics from Components (Client-Side)

In React components, when you call `sendHits` from an event handler (click, submit, etc.), the Flagship React SDK automatically collects hits into a pool and sends them in batch in the background on the client side. This provides optimal performance without blocking user interactions.

**How it works (Client-Side):**

1. User triggers an event (click, submit, etc.)
2. Call `useflagship().sendHits([...])` - hits are added to the client-side pool
3. Event handler completes immediately (non-blocking)
4. Flagship React SDK automatically batches and sends pooled hits in the background
5. User experience is never blocked by analytics

**Example: Track product clicks**

```typescript
// In app/components/ProductItem.tsx
import { useFlagship } from '@flagship.io/react-sdk/edge';
import { EventCategory, HitType } from "@flagship.io/react-sdk/edge";

export function ProductItem({product}: {product: ProductItemFragment}) {
  const flagship = useFlagship();

  const handleProductClick = () => {
    // Send hit - pooled and sent in batch automatically
    flagship.visitor?.sendHits([
      {
        type: HitType.EVENT,
        category: EventCategory.ACTION_TRACKING,
        action: 'product_click',
        label: product.title,
        value: 1,
      },
    ]);
    // Event handler completes immediately, hit sent in background
  };

  return (
    <Link
      className="product-item"
      to={`/products/${product.handle}`}
      onClick={handleProductClick}
    >
      {/* Product content */}
    </Link>
  );
}
```

**How Analytics Pooling Works**

**Server-Side (Loaders):**

```typescript
// 1. Multiple loaders add hits to server pool
loader1: await fsVisitor.sendHits([hit1, hit2]);
loader2: await fsVisitor.sendHits([hit3]);

// 2. Response sent to client immediately

// 3. In background, Flagship.close() flushes pool
waitUntil?.(Flagship.close());

// 4. All hits [hit1, hit2, hit3] sent in batch
```

**Client-Side (Components):**

```tsx
const flagship = useFlagship();
// 1. User clicks button
onClick={() => {
  // 2. Hit added to client-side pool
  flagship.sendHits([hit]);
  // 3. Handler completes immediately
}}

// 4. React SDK batches hits automatically
// 5. Pooled hits sent in background periodically
// 6. User never blocked by analytics
```

**Benefits:**

* **Non-blocking**: User experience never interrupted by analytics
* **Efficient**: Multiple hits batched into fewer network requests
* **Reliable**: Server-side uses `waitUntil` to ensure delivery
* **Automatic**: Client-side pooling handled by React SDK
* **Simple**: Same `sendHits` API for both server and client
* **Performant**: Reduces network overhead and improves responsiveness

### Configure Content Security Policy for Flagship

When using Flagship with Hydrogen, you need to configure the Content Security Policy (CSP) to allow connections to Flagship's domains. This is done in entry.server.tsx:

```typescript
// In app/entry.server.tsx
import type {
  AppLoadContext,
  EntryContext,
} from '@shopify/remix-oxygen';
import {RemixServer} from '@remix-run/react';
import {isbot} from 'isbot';
import {renderToReadableStream} from 'react-dom/server';
import {createContentSecurityPolicy} from '@shopify/hydrogen';

export default async function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
  context: AppLoadContext,
) {
  const {nonce, header, NonceProvider} = createContentSecurityPolicy({
    shop: {
      checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
      storeDomain: context.env.PUBLIC_STORE_DOMAIN,
    },
    // Allow connections to Flagship domains
    connectSrc: [
      'https://*.flagship.io',
      'https://cdn.flagship.io',
      'https://decision.flagship.io',
    ],
  });

  const body = await renderToReadableStream(
    <NonceProvider>
      <RemixServer context={remixContext} url={request.url} />
    </NonceProvider>,
    {
      nonce,
      signal: request.signal,
      onError(error) {
        console.error(error);
        responseStatusCode = 500;
      },
    },
  );

  if (isbot(request.headers.get('user-agent'))) {
    await body.allReady;
  }

  responseHeaders.set('Content-Type', 'text/html');
  responseHeaders.set('Content-Security-Policy', header);

  return new Response(body, {
    headers: responseHeaders,
    status: responseStatusCode,
  });
}
```

### Managing Bucketing Data

For optimal performance, Flagship uses bucketing data to make decisions locally without API calls. You can manage this data in several ways:

#### Development Approach

During development, you can use a static bucketing file:

1. Fetch the bucketing data from Flagship CDN:

```bash
# Replace YOUR_ENV_ID with your Flagship Environment ID
curl -s https://cdn.flagship.io/YOUR_ENV_ID/bucketing.json > app/helpers/bucketing.json
```

2. Import it in your flagship.ts helper:

```typescript
import initialBucketing from './bucketing.json';

export async function startFlagshipSDK() {
  return await Flagship.start(
    import.meta.env.VITE_ENV_ID,
    import.meta.env.VITE_API_KEY,
    {
      decisionMode: DecisionMode.BUCKETING_EDGE,
      initialBucketing, // Use the imported data
      // ...other config
    },
  );
}
```

#### Production Approach

For production environments, it's better to trigger a redeployment when campaigns are updated rather than committing changes to your repository:

1. Create a GitHub Action workflow file (`.github/workflows/update-and-deploy.yml`):

```yaml
name: Update Flagship Bucketing Data and Deploy

on:
  # Webhook from Flagship when campaigns change
  repository_dispatch:
    types: [flagship-campaign-updated]
  # Allow manual triggering
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
      
      - name: Fetch latest bucketing data
        run: |
          curl -s https://cdn.flagship.io/${{ secrets.FLAGSHIP_ENV_ID }}/bucketing.json > app/helpers/bucketing.json
      
      - name: Build application
        run: npm run build
      
      # For Shopify Oxygen deployment
      - name: Deploy to Shopify Oxygen
        run: |
          npx shopify hydrogen deploy
        env:
          SHOPIFY_HYDROGEN_DEPLOYMENT_TOKEN: ${{ secrets.SHOPIFY_HYDROGEN_DEPLOYMENT_TOKEN }}
```

2. Configure secrets in GitHub:
   * Go to your repository Settings > Secrets and variables > Actions
   * Add `FLAGSHIP_ENV_ID` with your Flagship environment ID
   * Add `SHOPIFY_HYDROGEN_DEPLOYMENT_TOKEN` with your deployment token
3. Set up a webhook in the Flagship Platform:
   * Go to your Flagship project settings
   * Add a webhook URL pointing to GitHub Actions
   * Configure it to trigger on campaign updates

This approach:

* Avoids cluttering your commit history with data changes
* Provides immediate updates to production when campaigns change
* Follows infrastructure-as-code best practices
* Works well with modern deployment platforms like Shopify Oxygen
* Maintains version control over your application code while keeping data fresh

> ⚠️ **Note**: If you're using a different hosting platform (Vercel, Netlify, AWS, etc.), replace the deployment step with the appropriate commands for your platform.

### Troubleshooting

#### Common Issues and Solutions

**1. CSP Violations**

**Problem**: Browser console shows CSP violations for Flagship domains.

**Solution**: Ensure all Flagship domains are in your CSP configuration:

```typescript
connectSrc: [
  'https://*.flagship.io',
  'https://cdn.flagship.io',
  'https://decision.flagship.io',
],
```

### Learn More

* [Flagship React SDK Documentation](/server-side/sdks/react.md#welcome-to-the-comprehensive-guide-for-flagship-reactjs-sdk)
* [Shopify Hydrogen Documentation](https://hydrogen.shopify.dev/)
* [Remix Documentation](https://remix.run/docs/en/main)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.abtasty.com/server-side/concepts/flagship-edge-worker-integration/flagship-+-shopify-hydrogen-integration.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
