How to deploy Shopify Hydrogen 2.0 to Vercel

Fri Mar 17 2023 - Jaxson Luu

How to deploy shopify hydrogen 2 to vercel

If you want to deploy your Shopify Hydrogen "2" (2023-01+) app to Vercel, you can do so by following these steps:

*Note: The updates below apply for Hydrogen version (2024.1.0 or higher)

1) In root folder, rename server.ts to server-dev.ts (for running on development).

2) Create new server.ts with content below:

// Virtual entry point for the app
import * as remixBuild from '@remix-run/dev/server-build';
import {createRequestHandler} from '@remix-run/server-runtime';
import {
  cartGetIdDefault,
  cartSetIdDefault,
  createCartHandler,
  createStorefrontClient,
} from '@shopify/hydrogen';

import {AppSession} from '~/lib/session.server';
import {getLocaleFromRequest} from '~/lib/utils';

/**
 * Export a fetch handler in module format.
 */
export default async function (request: Request): Promise {
  try {
    /**
     * This has to be done so messy because process.env can't be destructured
     * and only variables explicitly named are present inside a Vercel Edge Function.
     * See https://github.com/vercel/next.js/pull/31237/files
     */
    const env: Env = {
      SESSION_SECRET: '',
      PUBLIC_STOREFRONT_API_TOKEN: '',
      PRIVATE_STOREFRONT_API_TOKEN: '',
      PUBLIC_STORE_DOMAIN: '',
      PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID: '',
      PUBLIC_CUSTOMER_ACCOUNT_API_URL: '',
    };
    env.SESSION_SECRET = process.env.SESSION_SECRET as string;
    env.PUBLIC_STOREFRONT_API_TOKEN = process.env
      .PUBLIC_STOREFRONT_API_TOKEN as string;
    env.PRIVATE_STOREFRONT_API_TOKEN = process.env
      .PRIVATE_STOREFRONT_API_TOKEN as string;
    env.PUBLIC_STORE_DOMAIN = process.env.PUBLIC_STORE_DOMAIN as string;
    env.PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID = process.env
      .PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID as string;
    env.PUBLIC_CUSTOMER_ACCOUNT_API_URL = process.env.PUBLIC_CUSTOMER_ACCOUNT_API_URL as string;
    /**
     * Open a cache instance in the worker and a custom session instance.
     */
    if (!env?.SESSION_SECRET) {
      throw new Error('SESSION_SECRET process.environment variable is not set');
    }

    const [session] = await Promise.all([
      AppSession.init(request, [process.env.SESSION_SECRET as string]),
    ]);

    /**
     * Create Hydrogen's Storefront client.
     */
    const {storefront} = createStorefrontClient({
      buyerIp: request.headers.get('x-forwarded-for') ?? undefined,
      i18n: getLocaleFromRequest(request),
      publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
      privateStorefrontToken: env.PRIVATE_STOREFRONT_API_TOKEN,
      storeDomain: env.PUBLIC_STORE_DOMAIN,
      // storefrontId: process.env.PUBLIC_STOREFRONT_ID,
      // requestGroupId: request.headers.get('request-id'),
    });

    const cart = createCartHandler({
      storefront,
      getCartId: cartGetIdDefault(request.headers),
      setCartId: cartSetIdDefault(),
    });

    const handleRequest = createRequestHandler(remixBuild as any, 'production');

    const response = await handleRequest(request, {
      session,
      storefront,
      cart,
      env,
      waitUntil: () => Promise.resolve(),
    });

    if (response.status === 404) {
      /**
       * Check for redirects only when there's a 404 from the app.
       * If the redirect doesn't exist, then `storefrontRedirect`
       * will pass through the 404 response.
       */
      // return storefrontRedirect({request, response, storefront});
    }

    return response;
  } catch (error) {
    // eslint-disable-next-line no-console
    console.error(error);
    return new Response('An unexpected error occurred', {status: 500});
  }
}

3) Update remix.config.ts with content below:

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  appDirectory: 'app',
  ignoredRouteFiles: ['**/.*'],
  watchPaths: ['./public', './.env'],
  server:
    process.env.NODE_ENV === 'development' ? './server-dev.ts' : './server.ts',
  /**
   * The following settings are required to deploy Hydrogen apps to Oxygen:
   */
  publicPath: (process.env.HYDROGEN_ASSET_BASE_URL ?? '/') + 'build/',
  assetsBuildDirectory: 'dist/client/build',
  serverBuildPath: 'dist/worker/index.js',
  serverMainFields: ['browser', 'module', 'main'],
  serverConditions: ['worker', process.env.NODE_ENV],
  serverDependenciesToBundle: 'all',
  serverModuleFormat: 'esm',
  serverPlatform: 'neutral',
  serverMinify: process.env.NODE_ENV === 'production',
  postcss: true,
  tailwind: true,
  future: {
    v3_fetcherPersist: true,
    v3_relativeSplatpath: true,
  },
  serverNodeBuiltinsPolyfill: {
    modules: {
      buffer: true, // Provide a JSPM polyfill
      fs: "empty", // Provide an empty polyfill
      crypto: true,
      zlib: true,
      stream: true,
      events: true,
      https: true,
      http: true,
      net: true,
      tls: true,
      url: true
    },
    globals: {
      Buffer: true,
    },
  }
};

4) Modify remix.env.d.ts

import type {WithCache, HydrogenCart} from '@shopify/hydrogen';
import type {Storefront} from '~/lib/type';
import type {AppSession} from '~/lib/session.server';

declare global {
  /**
   * A global `process` object is only available during build to access NODE_ENV.
   */
  const process: {env: {NODE_ENV: 'production' | 'development'} & Env};

  /**
   * Declare expected Env parameter in fetch handler.
   */
  interface Env {
    SESSION_SECRET: string;
    PUBLIC_STOREFRONT_API_TOKEN: string;
    PRIVATE_STOREFRONT_API_TOKEN: string;
    PUBLIC_STORE_DOMAIN: string;
    PUBLIC_STOREFRONT_ID: string;
    PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID: string;
    PUBLIC_CUSTOMER_ACCOUNT_API_URL: string;
  }
}

declare module '@shopify/remix-oxygen' {
  /**
   * Declare local additions to the Remix loader context.
   */
  export interface AppLoadContext {
    waitUntil: ExecutionContext['waitUntil'];
    session: AppSession;
    storefront: Storefront;
    cart: HydrogenCart;
    env: Env;
  }
  
   /**
   * Declare the data we expect to access via `context.session`.
   */
   export interface SessionData {
    customerAccessToken: string;
  }
}

// Needed to make this file a module.
export {}

5) Update app/lib/session.server.ts

import {type HydrogenSession} from '@shopify/hydrogen';
import {
  createCookieSessionStorageFactory,
  createCookieFactory,
} from '@remix-run/server-runtime';
import type {
  SignFunction,
  UnsignFunction,
  type SessionStorage,
  type Session,
} from '@remix-run/server-runtime';

const encoder = new TextEncoder();

export const sign: SignFunction = async (value, secret) => {
  const data = encoder.encode(value);
  const key = await createKey(secret, ['sign']);
  const signature = await crypto.subtle.sign('HMAC', key, data);
  const hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace(
    /=+$/,
    '',
  );

  return value + '.' + hash;
};

export const unsign: UnsignFunction = async (cookie, secret) => {
  const value = cookie.slice(0, cookie.lastIndexOf('.'));
  const hash = cookie.slice(cookie.lastIndexOf('.') + 1);

  const data = encoder.encode(value);
  const key = await createKey(secret, ['verify']);
  const signature = byteStringToUint8Array(atob(hash));
  const valid = await crypto.subtle.verify('HMAC', key, signature, data);

  return valid ? value : false;
};

async function createKey(secret: string, usages: CryptoKey['usages']): Promise {
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    {name: 'HMAC', hash: 'SHA-256'},
    false,
    usages,
  );

  return key;
}

function byteStringToUint8Array(byteString: string): Uint8Array {
  const array = new Uint8Array(byteString.length);

  for (let i = 0; i < byteString.length; i++) {
    array[i] = byteString.charCodeAt(i);
  }

  return array;
}

/**
 * This is a custom session implementation for your Hydrogen shop.
 * Feel free to customize it to your needs, add helper methods, or
 * swap out the cookie-based implementation with something else!
 */
export class AppSession implements HydrogenSession {
  #sessionStorage;
  #session;

  constructor(sessionStorage: SessionStorage, session: Session) {
    this.#sessionStorage = sessionStorage;
    this.#session = session;
  }

  static async init(request: Request, secrets: string[]) {
    const createCookie = createCookieFactory({sign, unsign});
    const createCookieSessionStorage =
      createCookieSessionStorageFactory(createCookie);
    const storage = createCookieSessionStorage({
      cookie: {
        name: 'session',
        httpOnly: true,
        path: '/',
        sameSite: 'lax',
        secrets,
      },
    });

    const session = await storage
      .getSession(request.headers.get('Cookie'))
      .catch(() => storage.getSession());

    return new this(storage, session);
  }

  get has() {
    return this.#session.has;
  }

  get get() {
    return this.#session.get;
  }

  get flash() {
    return this.#session.flash;
  }

  get unset() {
    return this.#session.unset;
  }

  get set() {
    return this.#session.set;
  }

  destroy() {
    return this.#sessionStorage.destroySession(this.#session);
  }

  commit() {
    return this.#sessionStorage.commitSession(this.#session);
  }
}

Done! You should now be able to deploy your Shopify Hydrogen 2.0 to Vercel. Please config for your vercel Remix project like screenshot below:

vercel-config-hydrogen.png

Watch the video below If you can't deploy Hydrogen 2.0 to Vercel by following the steps above: