---
title: How to deploy Medusa on Vercel
description: Deploy a Medusa storefront on Vercel, including where the backend runs, which environment variables the current starter reads, and how to cache the catalog.
url: "https://vercel.com/kb/guide/deploy-medusa-on-vercel"
published: 2026-09-29
last_updated: 2026-09-29
authors: Vercel
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

A Medusa application deploys in two parts. The Next.js storefront runs on Vercel like any other [Next.js project](https://vercel.com/docs/frameworks/full-stack/nextjs). The Medusa server and worker are long-lived Node.js processes that run on a persistent host, alongside PostgreSQL and Redis.

Here's how to deploy the storefront, starting with that split and ending with the errors that appear after the first deploy.

## **Should you use Medusa or a hosted commerce platform?**

Medusa is an open-source headless commerce engine. It ships commerce logic as modules covering products, carts, orders, pricing, and fulfillment, and exposes them through a Store API and an Admin API that any frontend can call. You run the engine yourself, backed by PostgreSQL and Redis.

The choice between Medusa and a hosted platform such as Shopify comes down to how much of the commerce workflow you need to own:

| Factor            | Medusa fits when                                                                    | A hosted platform fits when                              |
| ----------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------- |
| Checkout workflow | You need to define the workflow in code, including custom steps and B2B quote flows | Standard checkout with extension points covers your flow |
| Data and schema   | You need direct access to the commerce schema and your own backup strategy          | Vendor-managed data is acceptable                        |
| Extensibility     | You want to write modules and workflows in Node.js                                  | The vendor's app ecosystem covers your requirements      |
| Operations        | Your team can run a server, worker, database, and Redis                             | You want the vendor to run the platform                  |

Choosing Medusa means running a server, worker, PostgreSQL instance, and Redis instance, all of which need to be live before the storefront can serve a product page.

## **What you need to deploy Medusa on Vercel**

Have these in place before you start:

- **Node.js runtime:** Version 20.19.0 or later, or 22.12.0 or later, and below version 25. The same range covers the backend and the storefront, per Medusa's [documented prerequisites](https://docs.medusajs.com/resources/nextjs-starter).
  
- **Medusa application:** Version 2.14.0 or later, created with `create-medusa-app`, which installs the `dtc-starter` monorepo. The archived standalone starter is not a supported starting point.
  
- **Medusa server and worker:** Both processes running on a persistent Node.js host, such as a container platform, a virtual machine, or Medusa Cloud. Medusa's [deployment guide](https://docs.medusajs.com/learn/deployment/general) puts the host at 2 GB of RAM or more. The storefront points at that host's public URL.
  
- **PostgreSQL and Redis:** Both provisioned alongside the backend. Redis backs the Event, Workflow Engine, Locking, and Caching modules in production.
  
- **Publishable API Key:** Created in Medusa Admin with at least one sales channel attached. The storefront sends it on every Store API request.
  
- **Vercel account:** Pro or Enterprise for multiple function regions or durations above 300 seconds. Hobby covers a single-region deploy.
  

With the backend running and reachable at a public URL, the storefront deployment takes a few minutes.

## **How a Medusa deployment splits across Vercel and a Node.js host**

Medusa's [worker mode](https://docs.medusajs.com/learn/production/worker-mode) runs two long-lived processes in production. A `server` process answers API and Admin requests, and a `worker` process runs subscribers, scheduled jobs, and background workflows.

Three properties of those processes put them outside what [Vercel Functions](https://vercel.com/docs/functions) are built for:

- **Queues:** Production Medusa runs Redis-backed Event, Workflow Engine, Locking, and Caching modules. A process has to stay running to consume them between requests.
  
- **Invocation:** A Vercel Function starts when a request arrives. The worker has no inbound request to start it, since subscribers fire on events and scheduled jobs fire on a clock.
  
- **Duration:** Background workflows and scheduled jobs are not bounded by a request. Functions on [Fluid compute](https://vercel.com/docs/fluid-compute) default to 300 seconds, with an [800-second maximum](https://vercel.com/docs/functions/limitations) on Pro and Enterprise and a 1,800-second extended maximum in beta.
  

Run the server and worker on a host built for persistent Node.js services, and deploy the Next.js storefront on Vercel. The Medusa Admin is a Vite single-page application that the backend serves at `/app` by default, so it deploys with the server rather than separately.

## **Step 1: Create a Medusa app with create-medusa-app**

Start new projects from the command-line rather than from a template deploy button:

```bash
npx create-medusa-app@latest my-store --with-nextjs-starter
```

This installs the `dtc-starter` monorepo, with the backend in `apps/backend` and the storefront in `apps/storefront`.

## **Step 2: Deploy the Medusa storefront to Vercel**

Connect the storefront directory to a new Vercel project through the dashboard, or from the command-line with the [Vercel CLI](https://vercel.com/docs/cli), Vercel's command-line interface:

```bash
cd apps/storefront
vercel
```

Set the project's root directory to `apps/storefront` so the build runs against the storefront rather than the monorepo root. The first deploy builds successfully and fails at runtime until the storefront's environment variables are set.

## **Step 3: Set the Medusa storefront environment variables**

The current `dtc-starter` reads `NEXT_PUBLIC_MEDUSA_BACKEND_URL` in its middleware and JS SDK configuration. The archived standalone starter used `MEDUSA_BACKEND_URL`. In the current starter, setting only the unprefixed variable leaves the SDK using its `http://localhost:9000` fallback. Set the prefixed variable to your deployed Medusa server’s public URL.

Set these four variables in your Vercel project with values appropriate to each environment:

- `NEXT_PUBLIC_MEDUSA_BACKEND_URL`: Set this to your deployed Medusa server’s public URL, such as `https://your-backend.example.com`.
  
- `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY`**:** Set this to the publishable key from Medusa Admin, such as `pk_your_key_here`. Associate the key with the sales channels your storefront uses.
  
- `NEXT_PUBLIC_DEFAULT_REGION`**:** Set this to the two-letter country code to use when country detection finds no match. The starter defaults to `dk` (Denmark). Choose a country included in one of your configured Medusa regions.
  
- `NEXT_PUBLIC_BASE_URL`**:** Set this to your storefront’s public URL, such as `https://your-store.example.com`.
  

The backend hostname is public in this architecture. What protects your Store API is the publishable key, its sales channel scope, and cross-origin resource sharing (CORS) rules on the backend. Any variable you add that is a genuine secret, such as a shared secret on a revalidation route, takes no `NEXT_PUBLIC_` prefix. Next.js inlines prefixed values into the JavaScript it sends to the browser. Set all four for Production, Preview, and Development in [project settings](https://vercel.com/docs/environment-variables) so preview deployments resolve a backend too.

## **Step 4: Run Vercel Functions near your Medusa database**

Vercel Functions default to Washington, D.C. (`iad1`). Storefront functions call the Medusa API, so choose a function region close to the Medusa server. Host the Medusa server close to PostgreSQL to reduce latency between the backend and its database. Static assets and eligible cached page responses are distributed through the CDN.

If the database runs elsewhere, [set the function region](https://vercel.com/docs/functions/configuring-functions/region) in `vercel.json`:

```json
{
  "$schema": "<https://openapi.vercel.sh/vercel.json>",
  "regions": ["fra1"]
}
```

Use `fra1` for Frankfurt or `sin1` for Singapore. Hobby projects run functions in a single region, Pro in up to five, and Enterprise in all of them.

Pinning functions to one region does not change where the rest of the request is served. Static files and cached pages still come from the CDN, and [Routing Middleware](https://vercel.com/docs/routing-middleware) deploys to all regions by default, so country detection stays close to the visitor. On Hobby, Routing Middleware runs in fewer regions.

## **Step 5: Cache the Medusa catalog with ISR**

Product and collection pages change rarely and carry most of the traffic, which makes them the right candidates for [Incremental Static Regeneration](https://vercel.com/docs/incremental-static-regeneration) (ISR). Since Next.js 15, `fetch` is not cached by default, so opt in and tag the request:

```tsx
const product = await fetch(`${process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL}/store/products/${id}`, {
  headers: { 'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY! },
  cache: 'force-cache',
  next: { tags: [`product-${id}`] },
})
```

A cache hit is served from the nearest of Vercel’s 19 compute-capable regions, reached through the 126 points of presence that terminate the connection and route it there. On a miss, concurrent requests for the same path collapse into one function invocation per region, and a revalidation updates every region within 300 milliseconds.

Keep cart and account state out of that cache. Any Server Component that reads cookies opts its whole route into request-time rendering. Wrap cart state, account UI, and cookie-based pricing in `<Suspense>` boundaries, and give pricing its own boundary.

To invalidate on catalog changes, follow Medusa's [revalidation guide](https://docs.medusajs.com/resources/nextjs-starter/guides/revalidate-cache) and add a subscriber on `product.created`, `product.updated`, and `product.deleted` that calls a route handler on the storefront. Inside the handler, call `revalidateTag`, which takes a second argument in [Next.js 16](https://nextjs.org/docs/app/guides/upgrading/version-16). Use time-based revalidation for anything that displays stock, since inventory changes do not emit the same product events.

## **Step 6: Wire CORS, sessions, and Stripe webhooks to Medusa**

These settings live on the Medusa backend and decide whether the deployed storefront can talk to it. Configure the following on your backend host:

- **Store and Admin origins:** Set `storeCors` to the storefront origin and `adminCors` to the Admin origin, both under `projectConfig.http` in [`medusa.config.ts`](https://docs.medusajs.com/learn/configurations/medusa-config). Leave off the trailing slash, since an origin that ends in one will not match.
  
- **Auth origins:** Set `authCors` to the storefront and Admin origins combined, because Medusa applies it to every route starting with `/auth`. Leaving one out blocks login from that origin while the rest of the storefront keeps working.
  
- **Preview origins:** Add the preview origins you intend to allow to `storeCors` and, where authentication requires it, `authCors`. Use exact origins or a narrowly scoped regular expression for your project’s preview domains. Avoid a blanket `/vercel\.app$/` rule, which also permits unrelated sites hosted on Vercel.
  
- **Session cookies:** The starter authenticates through the JS SDK, which keeps the session in a cookie. Any fetch you write outside the SDK needs `credentials: 'include'`, or that cookie never reaches the Store API. For custom server-side requests, follow the starter’s `getAuthHeaders()` pattern.
  
- **Stripe webhooks:** Point Stripe's [webhook endpoint](https://docs.medusajs.com/resources/commerce-modules/payment/webhook-events) at the Medusa server, where the payment module verifies the signature. Pointing it at the Vercel deployment URL leaves orders unpaid, because the payment event never reaches the engine.
  

For preview deployments, build Open Graph image URLs from `VERCEL_PROJECT_PRODUCTION_URL`, which is set in previews as well. The value omits the protocol scheme, so prepend `https://`. Point the preview-scoped backend URL at a staging Medusa server, or turn on [Deployment Protection](https://vercel.com/docs/deployment-protection) so preview traffic cannot reach production commerce data.

## **How to troubleshoot a Medusa deployment on Vercel**

Most first-week failures trace back to one of six causes. Work through them in order.

### **1\. The backend URL variable does not match the starter**

The build succeeds and routes return 404s or fetch errors because the storefront resolved its backend to `localhost`. The current starter reads `NEXT_PUBLIC_MEDUSA_BACKEND_URL`, and the unprefixed `MEDUSA_BACKEND_URL` still appears in places that predate it, including sections of Medusa's own storefront documentation. Set the prefixed name and redeploy.

### **2\. A trailing newline in the backend URL**

A newline pasted into the backend URL breaks the request URL built from that value and returns a 500 in production. Run `vercel env pull` and inspect the quoted value, since the stray character is invisible in the dashboard.

### **3\. The publishable key has no sales channel**

Product requests fail while the key itself is valid, because a publishable key returns nothing until a sales channel is attached. Attach one under **Settings** in Medusa Admin.

### **4\. Product images return 400**

`next/image` rejects images whose hostname is not listed in `images.remotePatterns`, and matching is exact down to protocol and pathname. Add the S3 or MinIO host in `apps/storefront/next.config.js`. The older `images.domains` field is deprecated since Next.js 14.

### **5\. The catalog stays stale after Admin changes**

Product pages keep the old price because nothing invalidated the tag. Confirm the subscriber runs on the worker process under the Redis Event Module rather than the in-memory one, and that the revalidation route returns a success response.

### **6\. A cold backend times out the function**

A `504` on the first request after a backend restart usually means a cold start stacked on a slow query. Raise `maxDuration` on the affected route segment, keeping in mind that values above 300 seconds require Pro or Enterprise, and treat a warm backend as the durable fix.

Working through these in order resolves most deployment issues without changes to the storefront code.

## **Next steps**

With the backend reachable and the storefront caching correctly, the deployment is ready to go live. [Start a new Vercel project](https://vercel.com/new) and connect the `apps/storefront` directory, or [browse the templates](https://vercel.com/templates) for a commerce starting point.

## Related resources

- [Vercel Functions](https://vercel.com/docs/functions)
  
- [Fluid compute](https://vercel.com/docs/fluid-compute)
  
- [Incremental Static Regeneration](https://vercel.com/docs/incremental-static-regeneration)
  
- [Configuring function regions](https://vercel.com/docs/functions/configuring-functions/region)
  
- [Environment variables](https://vercel.com/docs/environment-variables)
  
- [Deployment Protection](https://vercel.com/docs/deployment-protection)