Shopify Oxygen Environment Variables: Set, Pull, Deploy

On Shopify Oxygen, environment variables are set per environment in the Hydrogen channel (Storefront settings, then Environments and variables). Oxygen hands them to the worker on each request, and your code reads them from context.env in a loader or action, not from process.env. A deployment keeps the values it was built with, so any change needs a redeploy. The CLI’s env pull and env push sync a local .env.

Most of the trouble comes from three places: which environment a branch lands in, a changed value that production ignores, and an API key that slips into the browser bundle.

Which Oxygen environment does a branch deploy to?

Oxygen has three kinds of environment, and the Hydrogen environments docs map them straight onto Git branches. Production is the default branch, though you can point it at a different one. Custom environments are optional, and each is linked to its own branch. Preview catches every other branch.

Variables are scoped to those environments. When you add a variable you tick the environments it applies to, and you can add a second value for the same key that applies to other environments. So REVIEWS_API_KEY can hold a sandbox key for Preview and a live key for Production under one name, and the code never branches on where it is running.

Two limits matter before you plan a large setup. Each environment holds at most 110 unique variables. The number of public environments depends on the plan: development stores and trial plans get none, and Shopify Plus gets 25.

Which variables does Oxygen create for you?

A new storefront arrives with seven variables already set:

  • PRIVATE_STOREFRONT_API_TOKEN
  • PUBLIC_STOREFRONT_API_TOKEN
  • PUBLIC_STORE_DOMAIN
  • PUBLIC_STOREFRONT_ID
  • PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID
  • PUBLIC_CUSTOMER_ACCOUNT_API_URL
  • SESSION_SECRET

Every one except SESSION_SECRET is read-only. You can rotate the private Storefront API token from Storefront settings, but the docs are explicit that Oxygen does not redeploy anything when you do. Rotate the token, delete the old one, and the live deployment carries on with a token that no longer works until you push a commit. Do those steps in the opposite order to the one that feels natural: generate the new token, redeploy every environment, then delete the old one.

How do you read Oxygen environment variables in Hydrogen code?

An Oxygen deployment is a worker. The skeleton template’s server.ts exports a fetch(request, env, executionContext) handler, and that env argument is where your variables arrive. The template passes it to createHydrogenContext, which puts it on the context every loader and action receives.

Your own variables need declaring before TypeScript will let you touch them. The Hydrogen cookbook extends the global Env interface, and you can add to the template’s env.d.ts the same way:

// env.d.ts, below the existing reference lines
declare global {
  interface Env {
    REVIEWS_API_KEY: string;
    PUBLIC_REVIEWS_WIDGET_ID: string;
  }
}

export {};

Then read them on the server and decide, field by field, what goes to the browser:

// app/routes/products.$handle.tsx
import type {LoaderFunctionArgs} from 'react-router';

export async function loader({params, context}: LoaderFunctionArgs) {
  const {REVIEWS_API_KEY, PUBLIC_REVIEWS_WIDGET_ID} = context.env;

  if (!REVIEWS_API_KEY) {
    throw new Error('REVIEWS_API_KEY is not set for this environment');
  }

  const response = await fetch(
    `https://reviews.example.com/v1/products/${params.handle}`,
    {headers: {Authorization: `Bearer ${REVIEWS_API_KEY}`}},
  );
  const reviews = response.ok ? await response.json() : [];

  // Only these two fields are serialised into the page.
  return {reviews, reviewsWidgetId: PUBLIC_REVIEWS_WIDGET_ID};
}

The guard copies what the skeleton does for SESSION_SECRET: fail loudly with the variable’s name rather than send Bearer undefined to a third party and debug a 401.

Note what the PUBLIC_ prefix does in this loader. Nothing. context.env is a server-side object, and the browser receives exactly what the loader returns. The key stays private because it is never in the return value, and a PUBLIC_ value would stay on the server too if you left it out.

Why not import.meta.env?

Hydrogen builds with Vite, and Vite has its own environment handling. Its env and mode guide says variables prefixed with VITE_ are exposed in client-side code after bundling, and that they are replaced statically at build time. That is the wrong tool for Oxygen. A value inlined during the build ignores whatever you set per environment in the admin, and a VITE_ secret ends up in JavaScript any visitor can read. Keep runtime configuration on context.env.

Why didn’t my new value show up?

Because deployments are immutable. The environments page says it directly: changing a variable does not affect deployments made in the past, so you redeploy to pick it up.

This catches teams who are used to platforms where saving a variable restarts the app. On Oxygen, edit the value, then trigger a deploy for each environment that uses it. One more admin detail: ticking “Make this value secret” hides the value once it is saved, so keep the original somewhere your team can find it.

Keeping a local .env in step

Running shopify hydrogen dev on a project linked to a storefront loads that storefront’s variables into the local worker emulation, according to the hydrogen dev reference. The --env (environment handle) and --env-branch flags pick which environment’s set you get, and --env-file points at a file whose values override them.

When you want the values on disk, there is a pair of commands:

# Link this checkout to the storefront once
npx shopify hydrogen link

# Show environments and their handles
npx shopify hydrogen env list

# Write the staging branch's variables to .env (-f overwrites an existing file)
npx shopify hydrogen env pull --env-branch staging -f

# See what a push would change before it changes anything
npx shopify hydrogen env push --env staging --dry-run

The env pull reference accepts either --env with a handle or --env-branch with a branch name. env push takes --env only, so use the handle that env list printed. In this example the handle is assumed to be staging.

Treat env push with care. Shopify’s own announcement of the command warns that it can overwrite production variables, and -f skips the confirmation prompt. Use --dry-run every time, and push to a custom or preview environment before production.

Deploying from CI with the right variables

shopify hydrogen deploy needs an Oxygen deployment token, passed with --token or set as SHOPIFY_HYDROGEN_DEPLOYMENT_TOKEN, per the hydrogen deploy reference. The same command takes --env or --env-branch to choose the target, --preview to deploy to Preview, and --env-file to override variables for that one deployment.

That last flag suits a throwaway deployment against a test backend. As a habit it causes drift, because values from a CI file never appear in the admin, and the next person to check sees different config from the one that is running. Two other flags are worth having in a pipeline. --auth-bypass-token generates a token your end-to-end tests can use against the deployment, and --json-output (on by default) writes the deployment details to a file the next CI step can read.

What we would do

Make the Oxygen admin the single source of truth for every deployed environment. Pull from it for local work, declare every custom key on Env, read only through context.env, and fail with the variable’s name when one is missing. Redeploy after every change, rotation included. Run env push from a laptop only with --dry-run first, and never at production. Skip --env-file on deploys unless the deployment is disposable.

If you are weighing Oxygen against hosting a Hydrogen or Next.js storefront yourself, our Hydrogen vs Next.js comparison covers that decision. For the other half of an Oxygen deployment, the Hydrogen caching write-up explains sub-request and full-page caching.

We build and run Hydrogen storefronts on Oxygen, including the CI and environment set-up that keeps preview and production apart. If yours needs that work, our Shopify development page describes how we approach it.

Need this built properly?

Whoooop Ltd has spent 15+ years building and maintaining web applications in TypeScript, React, Node.js and serverless — the same ground this post covers.

Get in touch