Deploy to Vercel
You can run the whole stack in one Vercel project instead of on your own
cloud. The storefront and the subgraphs run as functions, and the gateway runs
as a container. A push to a branch deploys a preview of the whole stack, and a
merge to main deploys production.
This page covers what your project needs for that, and what works differently from a deployment to your own cloud, as in Deploying. You need a Vercel Pro team: container services and service bindings are not on the Hobby plan.
How the stack maps to Vercel
Section titled “How the stack maps to Vercel”| Part | Vercel service | Reachable from |
|---|---|---|
| Storefront | Next.js | The internet, for every path except /graphql |
| Gateway | A container, from the evolve-graphql-gateway image |
The internet, on /graphql and /graphql/auth |
| Each subgraph | A Node function | The gateway only |
flowchart LR browser[Browser] -- "/graphql" --> gateway[Gateway] browser -- "everything else" --> site[Storefront] site -- binding --> gateway gateway -- binding --> purchase[purchase] gateway -- binding --> discover[discover]
The subgraphs have no public route. The gateway reaches them through service bindings: Vercel puts the address of the subgraph in this deployment into an environment variable. A preview’s gateway therefore calls the preview’s subgraphs, without a URL that you set.
The browser calls /graphql on the host that served the page. One deployment
answers on several hostnames: its own URL, the branch alias and, in production,
your domain. A fixed gateway URL would be cross-origin from all but one of
them, and the browser would then drop the session cookies.
Describe the project in vercel.json
Section titled “Describe the project in vercel.json”vercel.json in the root of your repository declares the services, their
bindings and the routes. Nothing about the topology is configured in the
dashboard. For a project with the purchase and discover subgraphs:
{ "$schema": "https://openapi.vercel.sh/vercel.json", "regions": ["fra1"], "git": { "deploymentEnabled": true }, "services": { "site": { "root": "frontend/site", "framework": "nextjs", "bindings": [{ "type": "service", "service": "gateway", "format": "url", "env": "SERVER_API_GATEWAY_URL" }] }, "gateway": { "root": "backend/graphql-gateway", "runtime": "container", "entrypoint": "Dockerfile.vercel", "bindings": [ { "type": "service", "service": "purchase", "format": "url", "env": "SUBGRAPH_PURCHASE_URL" }, { "type": "service", "service": "discover", "format": "url", "env": "SUBGRAPH_DISCOVER_URL" } ] }, "purchase": { "root": "backend/purchase", "entrypoint": "src/vercel.ts" }, "discover": { "root": "backend/discover", "entrypoint": "src/vercel.ts" } }, "rewrites": [ { "source": "/graphql", "destination": { "service": "gateway" } }, { "source": "/graphql/auth", "destination": { "service": "gateway" } }, { "source": "/(.*)", "destination": { "service": "site" } } ]}regionssets where the functions run. Pick the region closest to your commerce engine.- The order of
servicesmatters. Vercel builds them one after another, in this order, on one checkout. The storefront goes first, because its build generates what the gateway needs; see Generate the schema. - Bind only what a service calls. Each binding adds to the time a cold function takes to start.
Serve a subgraph as a function
Section titled “Serve a subgraph as a function”Vercel calls a function’s default export once per request. Give each subgraph an entrypoint that builds the service, but does not start a server:
import type { IncomingMessage, ServerResponse } from "node:http";import { createApp } from "#src/server.ts";
const yoga = (await createApp()).getYogaInstance();if (!yoga) { throw new Error("The service has no GraphQL schema to serve");}
// Vercel prefers a handler's `fetch` over the handler itself, and the framework loses the request headers on that path.export default (req: IncomingMessage, res: ServerResponse) => yoga(req, res);createApp is the function your service already starts its server with. Export
a plain (req, res) function, not the yoga instance: the instance also has a
fetch method, and Vercel would call that with a web Request, whose headers
the framework’s request context does not copy.
Run the gateway as a container
Section titled “Run the gateway as a container”The gateway runs the same evolve-graphql-gateway image as on your own cloud.
Its container build only sees the directory of its Dockerfile, so that
directory holds everything the image needs:
FROM ghcr.io/evolve-platform/evolve-graphql-gateway:0.3.0
COPY --chown=app:app router.config.vercel.yaml /app/router.config.yamlCOPY --chown=app:app supergraph.generated.graphql /app/
# The supergraph is composed during the build, not read from Hive.ENV SUPERGRAPH_FILE_PATH=/app/supergraph.generated.graphqlStart router.config.vercel.yaml from the image’s own configuration, and
change two blocks:
docker run --rm --entrypoint cat ghcr.io/evolve-platform/evolve-graphql-gateway:0.3.0 /app/router.config.yaml > backend/graphql-gateway/router.config.vercel.yamloverride_subgraph_urls: subgraphs: purchase: url: expression: | origin = string!(env("SUBGRAPH_PURCHASE_URL")) if ends_with(origin, "/") { origin = slice!(origin, 0, -1) } origin + "/graphql" discover: url: expression: | origin = string!(env("SUBGRAPH_DISCOVER_URL")) if ends_with(origin, "/") { origin = slice!(origin, 0, -1) } origin + "/graphql"
plugins: federated_token: config: cookie_domain: "${COOKIE_DOMAIN:-}"- The subgraph addresses. A binding holds the origin of the subgraph, with
a trailing slash. Appending
/graphqlto it as it is gives//graphql, which Vercel answers with a redirect that the router does not follow. The router then reportsSUBGRAPH_RESPONSE_DESERIALIZATION_FAILURE, with no redirect in the message. WithLOG_LEVEL=debug, the router logs the URL it calls. - The cookie domain. Without a domain, the session cookies are host-only,
which is what works on every hostname of a deployment. The image substitutes
every
${...}in the file, and a variable without a default stops the router from starting, so giveCOOKIE_DOMAINan empty default.
Keep the rest of the federated token block as the image has it. The block
above shows only where cookie_domain sits.
Generate the schema
Section titled “Generate the schema”The subgraph schemas, the supergraph and the storefront’s types are generated
files, so a checkout does not have them. Vercel has no step before the
services build, so the storefront’s build generates them for every service.
Turbo already knows the order, from the codegen dependencies in each
package’s turbo.json:
"scripts": { "prebuild": "turbo run codegen --filter=@evolve-platform/site", "build": "node scripts/write-build-info.ts && next build --turbo"}The gateway’s codegen script calls task, which Vercel’s build image does
not have. Add @go-task/cli to the gateway’s dev dependencies, or have the
script call the Hive CLI itself.
To check the order, run turbo run codegen --filter=@evolve-platform/site --dry=text.
Change the storefront
Section titled “Change the storefront”Two settings that serve a self-hosted storefront stand in the way on Vercel:
output: process.env.VERCEL ? undefined : "standalone",cacheHandler: process.env.VERCEL ? undefined : fileURLToPath(new URL("./cache-handler.mjs", import.meta.url)),The cache handler shares the cache between the containers of a self-hosted storefront. Vercel has a cache of its own, and it does not bundle the handler, so a deployment that keeps it fails with “Cannot find module”.
Then let the browser find the gateway on its own origin. Make
CLIENT_API_GATEWAY_URL optional in the site’s configuration, keep the local
value in your .env, and fill in the origin where the fetcher is made:
// Without a configured gateway, it answers on the origin that served the page.const base = apiHostname ?? (typeof window === "undefined" ? "" : window.location.origin);const fetcher = useSessionClientFetcher(`${base}/graphql`, {Server-side rendering cannot call a relative URL. The binding sets
SERVER_API_GATEWAY_URL while the site runs, but not while it builds, so fall
back to the deployment’s own URL:
get serverApiHostname(): string | undefined { return ( this.SERVER_API_GATEWAY_URL || this.CLIENT_API_GATEWAY_URL || (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : undefined) );}The content security policy needs no change: 'self' already allows the
storefront’s own origin.
Set up the project
Section titled “Set up the project”-
Create the project and connect the repository.
vercel link --scope <your-team> --project <your-project>vercel git connectLeave the build, output and install commands empty:
vercel.jsonsets the framework to services. The Vercel GitHub app needs access to the organization that owns the repository. Without it,vercel git connectfails with a message about typos. -
Set the environment variables for Production and for Preview, with
vercel env add <name> <environment>or in the dashboard. Mark secrets as sensitive. Next to what your services read on any cloud, set these:Variable Value FEDERATED_TOKEN_ENCRYPT_KEY_1,FEDERATED_TOKEN_SIGN_KEY_132 bytes each. The gateway does not start without them. JWT_ISSUER,JWT_AUDIENCEOne fixed URL, the same for the gateway and the site, in every environment HOST_NAMEThe public hostname NPM_RCAn .npmrcthat can read the Evolve registry. Vercel writes it before the install.Leave these unset:
Variable Why CLIENT_API_GATEWAY_URL,COOKIE_DOMAINThe browser calls its own origin, and the cookies are host-only. SERVER_API_GATEWAY_URL,SUBGRAPH_*_URLThe bindings set them. REDIS_URLSee Redis. INTERNAL_EVENTS_TARGETThe services then publish to a no-op publisher. -
Push a branch. The deployment that follows is a preview. The first one takes a few minutes, most of it the install and the build of the storefront.
To find a build error faster than a deployment shows it, run vercel build
locally: it resolves the configuration the same way.
Deploy from git only. vercel deploy from your machine uploads the working
copy, which is over Vercel’s file limit with the build output in it, and
without .git the codegen in the build fails.
What is different from your own cloud
Section titled “What is different from your own cloud”| Your own cloud | Vercel | |
|---|---|---|
| Release | Blue-green, with smoke checks before the switch | One atomic deployment per push |
| Roll back | Move the traffic to the other label | vercel rollback, or promote an earlier deployment in the dashboard |
| Secrets | The cloud’s secret store, written by Terraform | The project’s environment variables |
| Supergraph | Published to Hive, after hive schema:check |
Composed during the build. Nothing checks it against the schema in use. |
| Previews | One test environment | A full stack for every branch |
Leave REDIS_URL unset for the subgraphs. Their cache and rate limiter then
keep their data in memory, per instance.
The framework’s Redis client keeps one TCP connection open and checks it every ten seconds. Vercel freezes a function between requests, so the check does not run and the connection dies unnoticed. The next request then waits for a timeout. A Redis provider for serverless does not help over TCP, only over HTTP, and the framework has no HTTP client for Redis.
In memory, the cache is smaller, and the rate limiter counts per instance and not for the whole deployment.
Events
Section titled “Events”The framework has no adapter for a Vercel queue. With INTERNAL_EVENTS_TARGET
unset, a service publishes its events nowhere, so nothing reacts to them: a
change in the CMS, for example, does not clear the storefront’s cache.
The storefront’s cache
Section titled “The storefront’s cache”The storefront’s cached fetches stay in Vercel’s data cache across deployments: a new deployment serves what the previous one cached. When a deployment changes the shape of cached data, invalidate the tags it uses:
vercel cache invalidate --tag layoutProduction and Preview have separate caches, but all previews share one.
Prefer invalidating a tag over vercel cache purge, which clears everything.

