---
title: How to deploy a Saleor storefront on Vercel
description: Deploy Saleor's Paper storefront template on Vercel, including environment variables, GraphQL caching, checkout, and production setup.
url: "https://vercel.com/kb/guide/how-to-deploy-a-saleor-storefront-on-vercel"
published: 2026-09-28
last_updated: 2026-09-28
authors: Vercel
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

Saleor splits into a Python backend and a Next.js storefront, and the storefront is the part that runs on Vercel. The [Saleor Commerce template](https://vercel.com/templates/next.js/nextjs-saleor-commerce) deploys it in one click, with the Saleor GraphQL API staying wherever it already runs, on Saleor Cloud or your own infrastructure.

Most of the configuration work sits in two places. Environment variables connect the storefront to your Saleor instance, and the caching model keeps catalog pages fast while the cart and checkout stay live on every request.

## **What is Saleor?**

Saleor is an open-source, GraphQL-only commerce platform. The core is a Python and Django application backed by PostgreSQL, with background workers handling asynchronous jobs. It exposes a single GraphQL endpoint at `/graphql/`, and channels are a platform primitive, so each channel carries its own currency and pricing.

Saleor and Medusa, the other open-source option in this series, differ most in language and API style. Medusa is Node.js and TypeScript with a REST API, so a Node-native team works in one language across the stack. Saleor runs Python on the backend and exposes GraphQL only, and it treats multi-channel and multi-currency as platform primitives rather than application logic.

Saleor fits best when your team already has Python and Django skills, treats a GraphQL-only API as an asset rather than a constraint, and needs multi-currency or multi-channel catalogs as platform primitives instead of application logic.

## **Where the three Saleor repositories run**

Saleor ships as three repositories, and only one of them deploys to Vercel.

Each has its own runtime:

- **Saleor Core:** The GraphQL API, written in Python and Django with PostgreSQL behind it. It runs on Saleor Cloud or your own infrastructure, never on Vercel, because it depends on long-lived background workers.
  
- **Saleor Dashboard:** The admin interface, a separate React and TypeScript application. It runs on Saleor Cloud alongside your instance, or as part of your own deployment when you self-host.
  
- **Paper storefront:** The customer-facing application, built on Next.js 16 and React 19. It runs on Vercel, and it's the only piece of the three that does.
  

On Vercel, the storefront calls the Saleor GraphQL API at both build time and request time. Your backend stays wherever it runs today, and the storefront points at it through one environment variable. That separation is why you can deploy the storefront without touching your Saleor instance.

## **What you need to deploy a Saleor storefront**

The one-click flow prompts for a handful of variables, and the deployment needs four things in place.

Gather the following:

- **A Saleor instance on version 3.23 or later:** Paper doesn't support older schemas. Saleor Cloud is the shorter path for most teams, and a Docker deployment run locally works the same way.
  
- **A channel slug from that instance:** Find it in the Saleor Dashboard under **Configuration** > **Channels**, then copy the slug value.
  
- **A Vercel account:** Hobby covers local experiments. Pro adds custom environments and Skew Protection.
  
- **Node.js and pnpm:** The template uses pnpm for installs and scripts, and pins the version it expects in the `packageManager` field of `package.json`.
  

Build-time request volume is the constraint that catches free sandboxes. During `next build`, the build calls Saleor for schema generation and prerendered content. Paper generates product pages on demand, so it does not prerender the entire catalog during every build.

The template throttles build-time GraphQL calls by defaulting `SALEOR_MIN_REQUEST_DELAY_MS` to 200 milliseconds during builds, but a large catalog on a free Saleor Cloud sandbox can still run into usage limits. Check Saleor's current [limits](https://docs.saleor.io/api-usage/usage-limits) before you rely on one for continuous integration builds.

## **How to deploy the Saleor storefront template**

With a Saleor instance running, the deployment itself is a clone and a set of environment variables.

### **1\. Clone the template**

Deploy directly from the [template page](https://vercel.com/templates/next.js/nextjs-saleor-commerce), or use the Saleor CLI to clone and wire up the instance in one step:

```bash
npm i -g @saleor/cli@latest
saleor storefront create --branch main --url https://your-instance.saleor.cloud/graphql/
```

The template clones from `github.com/saleor/storefront`, which is the version of record. It targets Next.js 16 with the App Router and React 19.

### **2\. Set the environment variables**

The storefront reads its configuration from a small set of variables, and each one has a different exposure.

Here's what each one does:

| Variable                         | Purpose                                                                                                                             | Exposure           |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| `NEXT_PUBLIC_SALEOR_API_URL`     | GraphQL endpoint, trailing slash required                                                                                           | Browser and server |
| `NEXT_PUBLIC_STOREFRONT_URL`     | Public HTTPS origin of the deployed storefront, used for canonical URLs and metadata. Defaults to `http://localhost:3000` if unset. | Browser and server |
| `NEXT_PUBLIC_DEFAULT_CHANNEL`    | Fallback channel slug, where `/` redirects                                                                                          | Browser and server |
| `NEXT_PUBLIC_DEFAULT_LOCALE`     | Default URL locale slug                                                                                                             | Browser and server |
| `NEXT_PUBLIC_STOREFRONT_LOCALES` | Enabled locale slugs                                                                                                                | Browser and server |
| `STOREFRONT_CHANNELS`            | Comma-separated channel allowlist, such as `us,uk,eu`                                                                               | Server only        |
| `SALEOR_APP_TOKEN`               | Privileged access for footer channel metadata                                                                                       | Server only        |
| `SALEOR_WEBHOOK_SECRET`          | Verifies the signature on incoming webhooks                                                                                         | Server only        |
| `REVALIDATE_SECRET`              | Protects manual calls to `/api/revalidate`                                                                                          | Server only        |

Never prefix `SALEOR_APP_TOKEN`, `SALEOR_WEBHOOK_SECRET`, or `REVALIDATE_SECRET` with `NEXT_PUBLIC_`, because Next.js inlines those values into the JavaScript it sends to the browser, and that can't be undone at runtime. On Vercel, every variable is either Config, which members with access can read back, or [Secret](https://vercel.com/docs/environment-variables/sensitive-environment-variables), which stays available to your deployments but can't be viewed again after saving. Store all three as Secret.

### **3\. Configure channel resolution**

The storefront decides which channels to serve in a fixed order, so set the variable that matches your setup:

1. `STOREFRONT_CHANNELS` takes precedence when set, which is the recommended configuration. The storefront serves only the slugs in the allowlist, and URLs for any other channel return 404.
   
2. `STOREFRONT_DISCOVER_CHANNELS=true` combined with `SALEOR_APP_TOKEN` pulls every active channel from the API. Instances carrying B2B, wholesale, or internal channels expose all of them this way, which is why the allowlist is the safer default.
   
3. `NEXT_PUBLIC_DEFAULT_CHANNEL` on its own produces a single-channel storefront. It's also the fallback channel in every configuration, and the root path redirects to it, so the footer channel selector hides itself automatically.
   

Set the one that matches your catalog, then deploy. Sessions run through Saleor's authentication SDK, and in a serverless runtime the rule is one client per request, built inside the request rather than shared across invocations.

## **Why Saleor caching happens at the component boundary**

Saleor sends every query as an HTTP POST to a single `/graphql/` endpoint. Products, categories, and collections all share that URL, so the requests never produce the distinct cacheable URLs a CDN matches on. A CDN in front of the Saleor API therefore leaves the catalog uncached.

Persisted queries can convert GraphQL operations into GET requests. The Paper template caches inside the React component tree instead.

### **Cache the catalog with Cache Components**

The template enables `cacheComponents` in its Next.js configuration, which makes the [`use cache`](https://nextjs.org/docs/app/api-reference/directives/use-cache) directive available in Next.js 16. Inside a cached function, `cacheLife` sets how long an entry survives and `cacheTag` names it for later invalidation.

Those two settings produce a display-cached, checkout-live split:

| Surface                                 | Freshness                                           | Why                                                                                  |
| --------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Product, category, and collection pages | `catalog`: 5-minute stale window, 1-hour revalidate | A slightly stale price on a browse page is harmless, and a slow page loses the visit |
| Navigation and footer menus             | `menus`: the same timings as `catalog`              | Menu edits arrive by webhook, so the profile only sets the fallback                  |
| Channel metadata                        | `channels`: 5-minute stale window, 1-day revalidate | Channel lists change rarely, so the fallback can sit further behind                  |
| Cart drawer                             | Always live                                         | Contents are per-session and must reflect real inventory                             |
| Checkout and payment                    | Always live                                         | Server actions compute totals and gateway state on every request                     |

Entries expire after a day on `catalog` and `menus`, and after a week on `channels`.

Product detail pages layer [Partial Prerendering](https://vercel.com/docs/partial-prerendering) on top. The product name, attributes, and metadata render into a static shell, while the variant gallery and add-to-cart control stream in through Suspense once search params resolve. Vercel serves that shell from the [Incremental Static Regeneration](https://vercel.com/docs/incremental-static-regeneration) (ISR) cache and streams the request-dependent parts from a [Vercel Function](https://vercel.com/docs/functions). A cached shell doesn't mean a free request, since the function still runs to fill the dynamic holes on most product page views.

### **Invalidate on catalog change**

A product edit in the Saleor Dashboard fires a webhook to `/api/revalidate`, which marks affected cache entries for revalidation. Subsequent requests refresh them according to the configured cache profile. The `cacheLife` TTLs above take over only when a webhook is missing or fails.

The template registers these tag patterns:

| Tag pattern             | Invalidated by                     |
| ----------------------- | ---------------------------------- |
| `product:{slug}`        | Product update, across all locales |
| `category:{slug}`       | Category update                    |
| `collection:{slug}`     | Collection update                  |
| `page:{slug}`           | Content page update                |
| `navigation:{channel}`  | Main menu change for the channel   |
| `footer-menu:{channel}` | Footer menu change for the channel |
| `channels`              | Channel list metadata change       |

On Saleor Cloud, install the Saleor Cloud Paper app from **Extensions** in the Dashboard, and it registers these webhooks for you. For direct HMAC webhooks, configure the same secret as the Saleor webhook’s `secretKey` and the storefront’s `SALEOR_WEBHOOK_SECRET`, and select the appropriate events and payload.

Vercel propagates an on-demand purge to all regions within 300 milliseconds, and the ISR cache persists content for 31 days or until you revalidate it. If a revalidation fails mid-flight, Vercel keeps serving the stale entry and sets a 30-second retry window, so a failed webhook call never becomes an error page.

## **Production setup for a Saleor storefront**

Catalog caching covers the browse experience only. Checkout, payment credentials, and per-branch environments each need configuration before a production rollout.

### **Checkout and payments**

Checkout v2 runs inside the Next.js application rather than redirecting to a hosted page. It uses the App Router, Server Components, and server actions, with shallow step URLs at `?step=contact`, `?step=shipping`, and `?step=payment` so the browser back button walks the funnel. None of it is cached.

Payments run through a registry of gateways, with Stripe supplied by the [Saleor Stripe App](https://docs.saleor.io/developer/app-store/apps/stripe/overview), a separate app that requires Saleor 3.23 or later. Stripe keys are handled across three places:

- **In the Saleor Stripe App:** In the Saleor Stripe App, configure the publishable key and a restricted key with the required permissions, then map the configuration to your Saleor channel. The app creates and manages its Stripe webhook.
  
- **In the storefront environment:** Set environment variables, including `NEXT_PUBLIC_ENABLE_STRIPE_PAYMENTS` and its server-side mirror `ENABLE_STRIPE_PAYMENTS`. No Stripe keys of any kind.
  
- **At runtime:** The publishable key, which the storefront receives from Saleor's `paymentGatewayInitialize` response rather than from its own build.
  

Neither `pk_` nor `sk_` values belong in the storefront's environment variables. To test the full checkout flow without Stripe, the dummy payment gateway enables itself in development, and `ALLOW_DUMMY_PAYMENT` with `NEXT_PUBLIC_ALLOW_DUMMY_PAYMENT` controls it elsewhere. Leave both unset in production.

### **Preview environments**

Each Vercel environment carries its own `NEXT_PUBLIC_SALEOR_API_URL`, so Production points at your live Saleor API while Preview points at a sandbox. Branch-scoped [environment variables](https://vercel.com/docs/environment-variables) go one level further, pinning a single feature branch to its own Saleor sandbox without touching the shared Preview value.

Two details decide whether preview environments resolve correctly. Logic that needs to distinguish a custom `staging` environment from an ordinary preview should read `VERCEL_TARGET_ENV`, because [`VERCEL_ENV`](https://vercel.com/docs/environment-variables/system-environment-variables) reports `preview` for both. Staging environments that rebuild the whole catalog also need a paid Saleor sandbox or a self-hosted instance behind them.

[Skew Protection](https://vercel.com/docs/skew-protection) is enabled by default on projects created after November 19, 2024 that use a supported framework, and Next.js 14.1.4 or newer needs no extra configuration. Older projects turn it on under **Settings** > **Advanced**. It's available to Pro and Enterprise teams.

Skew Protection pins framework-managed requests, meaning static assets, client-side navigations, and prefetches, to the deployment that served the page. Full-page navigations aren't pinned by default, so a hard refresh after a new deployment loads the latest version. A multi-step checkout that shouldn't reload mid-session pins document navigations with the `__vdpl` cookie, set in Routing Middleware.

## **How to troubleshoot a Saleor deployment**

Most Saleor deployment failures trace back to a short list of causes.

Work through these first:

- **A missing trailing slash on the API URL:** `NEXT_PUBLIC_SALEOR_API_URL` has to end in a slash. Set it to `https://your-instance.saleor.cloud/graphql/`, exactly as Saleor prints the endpoint.
  
- **A privileged token behind a public prefix:** Adding `NEXT_PUBLIC_` to `SALEOR_APP_TOKEN` inlines the token into the browser bundle at build time and hands it to every visitor. Leave the name unprefixed so the value stays server-side.
  
- **Request data read inside a cached function:** Calling `cookies()` or `headers()` within a `use cache` scope throws an error, because cached output can't depend on one request. Read session state outside the cached function and pass the result in as an argument.
  
- **Tokens expiring after the 3.23 upgrade:** Saleor 3.23 removed the `JWT_EXPIRE` setting that let tokens live indefinitely, so a client with no refresh path starts failing once its token ages out. Confirm the refresh flow works before upgrading.
  
- **Webhooks arriving without a verified signature:** The storefront can't verify incoming webhooks until `SALEOR_WEBHOOK_SECRET` is set. Set it before pointing Saleor webhooks at `/api/revalidate`.
  

To confirm the wiring works, rename a product in the Saleor Dashboard and reload its storefront page a few seconds later. The template also exposes `/api/cache-info`, which reports the cache tags currently in play.

## **When Saleor is not the right fit**

A few situations make Saleor harder to justify.

Consider another platform when:

- **Your backend team is Node.js-native:** Running a Python and Django service adds an operational surface some teams don't want. The Medusa guide in this series covers the Node and TypeScript path.
  
- **Your existing tooling expects REST:** Saleor is GraphQL-only, so integrations built around REST endpoints need an adapter layer.
  
- **You don't want to run the backend yourself:** Saleor Cloud is the managed path. Self-hosting means operating PostgreSQL and the background worker queue, which is infrastructure work outside what Vercel manages.
  

The two repositories also carry different licenses. Saleor Core is BSD-3-Clause, while the Paper storefront uses FSL-1.1-ALv2, which converts to Apache 2.0 after two years.

## **Next steps**

With your Saleor instance reachable and the environment variables set, the storefront is ready to deploy. [Start a new Vercel project](https://vercel.com/new) from your storefront repository, or [browse the templates](https://vercel.com/templates) to compare the Saleor template against the other commerce starters.

## **Related resources**

- [Next.js Saleor Commerce template](https://vercel.com/templates/next.js/nextjs-saleor-commerce)
  
- [Incremental Static Regeneration](https://vercel.com/docs/incremental-static-regeneration)
  
- [Partial Prerendering](https://vercel.com/docs/partial-prerendering)
  
- [Environment variables](https://vercel.com/docs/environment-variables)
  
- [Skew Protection](https://vercel.com/docs/skew-protection)
  
- [Deploy a headless Shopify storefront with Vercel](https://vercel.com/kb/guide/deploy-headless-shopify-storefront-with-vercel)