This is the full developer documentation for Evolve Platform Documentation
# The composable commerce platform built for the agentic era
> Everything you need to build and run digital commerce: one consistent API layer serving storefronts, mobile apps, and AI agents alike.
Any capability available to your storefront is automatically available to AI agents through the [Model Context Protocol (MCP)](/architecture/mcp/), with the same pricing, promotions, inventory, and business rules applied. That makes Evolve the ideal backend for **Composable Agentic Commerce**.
## Explore the docs
[Section titled “Explore the docs”](#explore-the-docs)
[Platform](/architecture/)How Evolve is built: domain services, data model, GraphQL Federation, and infrastructure.
[Getting started](/evolve/)Installation, frontend and backend development, deployments, and day-to-day workflows.
[MCP & AI agents](/architecture/mcp/)How Evolve exposes commerce capabilities to AI agents through the Model Context Protocol.
[AI authoring](/evolve/authoring/)AI-driven content authoring on the live storefront, with one-click push to your CMS and a headless MCP tool surface.
[GraphQL API reference](/reference/graphql/)The full federated GraphQL API: every query, mutation, and type, generated from the composed supergraph.
[Changelog](/changelog/)Notable changes, new features, and improvements across Evolve releases.
## What Evolve gives you
[Section titled “What Evolve gives you”](#what-evolve-gives-you)
AI & agents
AI integration through [MCP](/architecture/mcp/), [AI-driven authoring](/evolve/authoring/) on the live storefront, and [headless authoring tools over MCP](/evolve/authoring/mcp/) for external AI clients.
Storefront & UX
A modern, accessible, cross-device storefront with SEO built in, plus multi-language, multi-currency, and [multi-store](/architecture/core/store-context/) support from one backend.
Commerce & checkout
Catalog, faceted search, cart, discounts, [multiple payment providers](/architecture/payment/), and full [B2B and B2C](/architecture/b2b/) from the same backend.
Content management
Content for marketing and product pages, with pluggable [CMS support](/evolve/backend-services/content/) (Contentful, Storyblok) and live editing.
Customer & account
Registration, login, and account management with [federated authentication](/architecture/infrastructure/authentication/) and [transactional emails](/architecture/infrastructure/email/).
Platform & infrastructure
[GraphQL Federation](/architecture/core/graphql/), [event-driven messaging](/architecture/infrastructure/messaging/), [caching](/architecture/infrastructure/caching/) everywhere, and hosting on [AWS, GCP, or Azure](/architecture/cloud/).
## Architecture
[Section titled “Architecture”](#architecture)
Evolve’s GraphQL Federation layer is the single integration point for all channels. Storefronts, mobile apps, and AI agents all connect to the same gateway and share the same business logic.
```
graph TD;
agents["AI agents"]-->mcp["MCP server"]
storefront["Storefront"]-->gateway["GraphQL Gateway"]
mobile["iOS & Android native apps"]-->gateway
mcp-->gateway
gateway-->content["content"]
gateway-->catalog["catalog"]
gateway-->checkout["checkout"]
gateway-->order["order"]
gateway-->account["account"]
content-->cms["CMS"]
catalog-->pim["PIM"]
catalog-->search["search engine"]
catalog-->commerce["commerce engine"]
checkout-->commerce
order-->commerce
account-->commerce
cms-->cloud["Cloud native infrastructure (AWS / Azure / GCP)"]
pim-->cloud
commerce-->cloud
vendors["+30 vendors"]-->cloud
```
## Who is Evolve for?
[Section titled “Who is Evolve for?”](#who-is-evolve-for)
Evolve targets mid-size and enterprise companies running B2C, B2B, or combined commerce. It is built for teams that need stability, predictability, and speed, and want a platform they can shape to their business rather than the other way around.
This includes teams building on a new stack as well as teams already running a commerce engine, such as commercetools, Salesforce Commerce Cloud, or SAP Commerce Cloud, that want to modernize their experience layer without replacing the backend. See [Modernizing an existing stack](/architecture/modernization/).
## How Evolve relates to commercetools
[Section titled “How Evolve relates to commercetools”](#how-evolve-relates-to-commercetools)
commercetools is a commerce engine. It provides the headless APIs for products, carts, orders, and customers. Evolve is the platform that turns commercetools (and other MACH services) into a fully working commerce system. Evolve adds the [GraphQL Federation](/architecture/core/graphql/) API layer, the [Next.js storefront](/architecture/frontend/), domain services for [checkout](/evolve/backend-services/checkout/), [content](/evolve/backend-services/content/), and [search](/architecture/infrastructure/search/), payment orchestration, email notifications, [AI agent integration](/architecture/mcp/), cloud-native infrastructure, and more. commercetools is one of the building blocks that Evolve composes, not the other way around.
commercetools is the reference engine, and Evolve supports other engines the same way, including Salesforce Commerce Cloud and SAP Commerce Cloud. See [commerce engines](/architecture/core/commerce-engines/).
## Vendor and cloud independent
[Section titled “Vendor and cloud independent”](#vendor-and-cloud-independent)
Evolve’s [domain services](/architecture/core/design/) define their own [data model](/architecture/core/datamodel/) and business logic. The underlying commerce engine and other SAAS providers can be swapped or combined without affecting your channels. Evolve can also run on top of a commerce engine you already operate, adding a modern experience layer without replacing it. Today Evolve integrates with providers including commercetools, Algolia, Adyen, Buckaroo, Stripe, PayNL, Storyblok, and Contentful. It deploys to [AWS, GCP, or Azure](/architecture/cloud/). Any service can be added through the evolvable architecture based on [GraphQL Federation](/architecture/core/graphql/).
For a complete list of available building blocks, see the [MACH integrations overview](/architecture/integrations/).
## Why is Evolve called Evolve?
[Section titled “Why is Evolve called Evolve?”](#why-is-evolve-called-evolve)
Having built distributed, cloud-native systems for over 15 years, we’ve learned what it takes to build something that lasts. And what lasts are usually things that can evolve over time and deal with changing demands in elegant ways, without duct tape.
This is what we’ve seen with composable systems as well. The platform you start with will start changing the moment you take it to production. Good ones make this continuous change easy.
To accommodate our clients in their composable journeys, we’ve designed Evolve to be open to change and evolve with us and our clients.
So when Werner Vogels published his article [Monoliths are not dinosaurs](https://www.allthingsdistributed.com/2023/05/monoliths-are-not-dinosaurs.html) and voiced his belief that *“Building evolvable software systems is a strategy, not a religion. And revisiting your architectures with an open mind is a must.”*, we took this as inspiration for our name.
# Page not found
> This page does not exist.
# Platform
Evolve is a distributed system consisting of a varying number of components, depending on the implementation. It is intended to serve different compositions, depending on the use case that it implements. For example, a B2B use case most likely uses different components than a B2C case.
Through this model we are able to support any combination of SAAS services backing the domain services in Evolve.
## How it works
[Section titled “How it works”](#how-it-works)
Through a layered architecture, Evolve is able to completely decouple front-ends from the back-end, which in turn consists of several domain services that integrate different SAAS services and often also on-prem/custom services that are relevant to the use case.
GraphQL Federation is the key component that enables this.
```
graph TD;
storefronts-->graphql-federation-gateway
mcp-servers["MCP Servers"]-->graphql-federation-gateway
graphql-federation-gateway-->cms-storyblok
cms-storyblok-->Storyblok
graphql-federation-gateway-->catalog-commercetools
catalog-commercetools-->Algolia
catalog-commercetools-->commercetools
commercetools<-->bluestone-commercetools
bluestone-commercetools<-->BluestonePIM
graphql-federation-gateway-->account-commercetools
account-commercetools-->commercetools
graphql-federation-gateway-->checkout-commercetools
checkout-commercetools-->commercetools
checkout-commercetools-->payment-adyen
checkout-commercetools-->payment-buckaroo
checkout-commercetools-->payment-mollie
checkout-commercetools-->payment-stripe
checkout-commercetools-->payment-paynl
checkout-commercetools-->payment-invoice
payment-adyen-->commercetools
payment-adyen-->Adyen
payment-buckaroo-->commercetools
payment-buckaroo-->Buckaroo
payment-mollie-->commercetools
payment-mollie-->Mollie
payment-stripe-->commercetools
payment-stripe-->Stripe
payment-paynl-->commercetools
payment-paynl-->PayNL
payment-invoice-->commercetools
graphql-federation-gateway-->order-commercetools
order-commercetools-->commercetools
```
## AI integration through MCP
[Section titled “AI integration through MCP”](#ai-integration-through-mcp)
Beyond traditional storefronts, Evolve’s GraphQL Federation layer also serves as the foundation for AI integrations. Through the [Model Context Protocol (MCP)](/architecture/mcp/), Evolve exposes its commerce capabilities as tools that AI assistants can discover and invoke. MCP servers sit alongside storefronts as clients of the GraphQL gateway, enabling conversational commerce, AI-powered customer support, and autonomous workflows without duplicating any business logic.
## Explore the platform
[Section titled “Explore the platform”](#explore-the-platform)
[Core architecture](/architecture/core/)Core architectural concepts: domain services, data model, GraphQL Federation, and the framework that ties them together.
[Infrastructure](/architecture/infrastructure/)Cross-cutting infrastructure: the GraphQL gateway, authentication, caching, messaging, webhooks, email, and search.
[Frontend](/architecture/frontend/)The Next.js storefront: rendering strategy, routing, CMS abstraction, and image optimization.
[Payment architecture](/architecture/payment/)How payment providers integrate with the checkout service and commercetools.
[Model Context Protocol (MCP)](/architecture/mcp/)Evolve's Model Context Protocol (MCP) capabilities for exposing platform functionality to AI agents.
[B2B & B2C architecture](/architecture/b2b/)How Evolve runs B2C and B2B commerce from the same backend, letting you build sites that are B2C-only, B2B-only, or a combination of both.
[Cloud deployment](/architecture/cloud/)How Evolve deploys to AWS, GCP, and Azure using containers, Terraform, and MACH Composer.
[Design principles](/architecture/principles/)The design principles behind Evolve's architecture and how they shape the platform.
[MACH integrations](/architecture/integrations/)
[Background articles](/architecture/recommended-reading/)Recommended background articles to read.
# Authoring
> Evolve's AI-driven content authoring layer, what it does and how it fits into the platform. Editors describe what they want in chat, the AI emits structured page data, and one click pushes the result to your CMS.
Upcoming
The authoring stack is in active development and not yet part of a platform release. APIs described here may still shift before the first stable release.
Evolve’s authoring layer adds an AI-driven content editor on top of your storefront. Editors open a chat panel inside the live site, ask for a page or a change, and the AI emits structured page data that renders inline through the same components shoppers see. When the editor is happy, one click writes the result back to your CMS.
The authoring layer ships as five `@evolve-framework/*` packages you add to your Evolve project. It is opt-in: storefronts that don’t install it look and behave exactly like a regular Evolve site.
## What you get
[Section titled “What you get”](#what-you-get)
* **Conversational page authoring**: editors describe what they want and the AI builds a page (or edits an existing one). Output is structured page data, not raw markup, so the storefront renders it with the same components it uses for live traffic.
* **Inline editing**: text and image fields on the rendered page become directly editable, with overlays that show which block the cursor is in.
* **Library reuse**: AI tools search your commerce catalog and your CMS library so generated pages compose existing entries, brand assets, and real SKUs instead of inventing strings.
* **Image and video generation**: when nothing in the library fits, the AI calls a provider (OpenAI, Google, fal) to produce an asset. Bytes stay client-side until the editor pushes, so you never pay for storage on rejected drafts.
* **Headless tool surface**: the same AI tools the chat panel uses are also served over the [Model Context Protocol](/architecture/mcp/), so external agents (Claude Desktop, Cursor, custom AI clients) can drive content creation through the same gateway, same auth, and same persisted queries as the in-browser chrome.
* **Headless workflows** *(optional addon)*: long-running content jobs (campaign hubs, bulk page generation) run as background flows triggered by API or MCP, landing drafts in your CMS for editorial review. See [Workflows](/evolve/authoring/workflows/).
* **Push to CMS**: one click writes the full page tree (entries plus assets) through the federation gateway to your CMS in the shape your storefront’s SSR expects.
## How it fits
[Section titled “How it fits”](#how-it-fits)
```
graph LR
Editor[Editor in browser] --> Chrome[Chrome UI authoring-chrome]
Headless["Headless AI clients Claude Desktop, Cursor, agents"] --> MCP
Chrome --> Server[Authoring server authoring-server]
MCP["/mcp endpoint"] --> Server
Server --> AI[AI providers authoring-ai-providers]
Server --> Gateway[GraphQL gateway]
Gateway --> CMS[CMS subgraph]
Workflows[Workflows addon authoring-workflows optional] -. opt-in .-> Server
Workflows --> CMS
```
The chrome is a React UI that overlays the live storefront. It calls the authoring server for chat, library reads, and pushes. The server delegates AI work to the providers package and writes to the CMS through your existing [federation gateway](/architecture/core/graphql/): same auth, same schema, same routing as your storefront’s reads. The CMS half of the stack is `ContentfulAuthoringModule` (from `@evolve-framework/contentful/authoring`, composed into your CMS service via `CompositeModule`), and the gateway forwards the editor’s CMS token through the `authoring_token` router plugin. See the [installation guide](/evolve/authoring/installation/) for the wiring.
The MCP endpoint is opt-in but recommended: it lets headless clients drive the same tool surface as the chat panel, with tools generated from a project-owned GraphQL operations file via `@evolve-framework/mcp-core` codegen.
Workflows are a fully separate optional addon, mounted on the same Fastify host as the server via `mountWorkflows(app, opts)` for headless content runs. Standard installs do not need it.
## Setting it up
[Section titled “Setting it up”](#setting-it-up)
The authoring layer is installed and configured as part of [working with Evolve](/evolve/):
* [Getting started](/evolve/authoring/getting-started/): install the packages and wire up your first editor session in about ten minutes.
* [Installation guide](/evolve/authoring/installation/): the full installation for an existing Evolve project (CMS service, authoring service, gateway, and storefront).
* [Packages](/evolve/authoring/packages/): what each `@evolve-framework/*` package does and when you need it.
* [MCP endpoint](/evolve/authoring/mcp/): expose the AI tools to headless clients through `@evolve-framework/mcp-core` codegen.
* [Workflows](/evolve/authoring/workflows/) *(optional)*: the addon for long-running, headless content jobs.
# B2B & B2C architecture
> How Evolve runs B2C and B2B commerce from the same backend, letting you build sites that are B2C-only, B2B-only, or a combination of both.
Evolve runs B2B and B2C commerce from the same backend. A single set of services handles both models, so you can build a site that is B2C-only, B2B-only, or (increasingly common) a combination of both. B2B functionality layers on top of the standard commerce model, adding business units, associates with roles, quote workflows, and shared shopping lists. The path through the code depends on whether the authenticated user has an active business context.
## How it builds on StoreContext
[Section titled “How it builds on StoreContext”](#how-it-builds-on-storecontext)
Every request in Evolve carries a [StoreContext](/architecture/core/store-context/) containing a store key, locale, and currency that scope the request to the right store. For B2C customers this is all that’s needed: the StoreContext determines which assortment, prices, and content they see.
B2B adds a second layer. When a user belongs to a business unit, Evolve attaches a **BusinessContext** to their session. The BusinessContext can override the store key from the StoreContext with the business unit’s own store, giving B2B customers store-specific pricing and assortments without requiring a separate deployment.
## BusinessContext
[Section titled “BusinessContext”](#businesscontext)
When a B2B user logs in, Evolve loads their business units and selects a default. The active business unit and its associated store are stored in the user’s session token as the **BusinessContext**:
```typescript
type BusinessContext = {
businessUnitId: string;
businessUnitKey: string;
storeKey?: string;
storeId?: string;
channelId?: string;
};
```
The BusinessContext affects the entire request:
* **Store key override**: the business unit’s store replaces the default StoreContext store key, giving B2B customers store-specific assortments and pricing
* **Distribution channel**: the business unit’s channel enables B2B-specific pricing through commercetools price channels
* **API scoping**: operations route through the Associate API instead of the customer `me` endpoint
Users with access to multiple business units can switch between them using the `setActiveBusinessContext` mutation:
```graphql
mutation {
setActiveBusinessContext(
businessUnitId: "acme-corp"
storeKey: "b2b-store"
) {
success
}
}
```
## Dual-path routing
[Section titled “Dual-path routing”](#dual-path-routing)
Every operation that can be performed in both B2C and B2B contexts uses a dual-path pattern. The service checks whether the user has an active business context and routes accordingly:
```
graph TD;
request["Incoming request"]
check{"Has BusinessContext?"}
b2c["Customer API me endpoint"]
b2b["Associate API scoped to business unit"]
backend["commercetools"]
request --> check
check -- "No (B2C)" --> b2c
check -- "Yes (B2B)" --> b2b
b2c --> backend
b2b --> backend
```
* **B2C path**: uses the customer’s own OAuth token via the `me` API. Operations are scoped to the individual customer.
* **B2B path**: uses system credentials via the Associate API, scoped to the associate’s ID and business unit key. This enforces the commerce backend’s built-in permission model for associates.
This pattern applies to orders, shopping lists, quotes, carts, and other operations that differ between B2C and B2B.
## Business units
[Section titled “Business units”](#business-units)
A business unit represents a company or organizational entity. It contains associates (users), addresses, and is linked to one or more stores.
```graphql
type BusinessUnit {
id: ID!
name: String!
stores: [Store!]
addresses(page: Int, pageSize: Int): AddressConnection!
billingAddress: Address
associates(page: Int, pageSize: Int): BusinessUnitAssociateConnection!
availableRoles: [BusinessUnitRole!]!
orders(page: Int, pageSize: Int): OrderResult
shoppingLists(page: Int, pageSize: Int): ShoppingListResult
quoteRequests(page: Int, pageSize: Int): QuoteRequestResult
quotes(page: Int, pageSize: Int): QuoteResult
}
```
The `BusinessUnit` type is a federated entity extended by multiple services. The account service owns the core fields (name, stores, addresses, associates), while the order, quote, and shopping list services extend it with their respective fields using GraphQL Federation.
### Associates and roles
[Section titled “Associates and roles”](#associates-and-roles)
Associates are users linked to a business unit. Each associate has one or more roles that determine their permissions:
```graphql
type BusinessUnitAssociate {
id: ID!
email: EmailAddress!
givenName: String
familyName: String
roles: [BusinessUnitRole!]!
}
```
Associates can be managed through mutations:
* **`businessUnitAssociateInvite`**: invite a new associate by email with specific roles
* **`businessUnitAssociateRolesUpdate`**: change an associate’s roles
* **`businessUnitAssociateRemove`**: remove an associate from the business unit
## Quotes
[Section titled “Quotes”](#quotes)
The quote workflow allows B2B customers to request pricing for a cart before placing an order.
```
graph LR;
cart["Cart"]
request["Quote Request"]
quote["Quote"]
order["Order"]
cart -- "quoteRequestCreate" --> request
request -- "Seller responds" --> quote
quote -- "Customer accepts" --> order
```
A quote request is created from an existing cart:
```graphql
mutation {
quoteRequestCreate(
id: "cart-id:cart-version"
args: {
comment: "Requesting volume discount"
purchaseOrderNumber: "PO-2024-001"
}
) {
quoteRequest { id { id } status }
}
}
```
Quote requests and quotes are accessed through the `BusinessUnit` type:
```graphql
query {
businessUnit(id: "acme-corp") {
quoteRequests(page: 1, pageSize: 10) {
results {
id { id }
status
comment
lineItems { name, quantity, price { gross { centAmount } } }
}
}
}
}
```
## Shopping lists
[Section titled “Shopping lists”](#shopping-lists)
Shopping lists work for both B2C and B2B, but with different scoping. B2C customers have a single implicit shopping list. B2B users can create multiple named shopping lists that are shared within their business unit.
```graphql
type ShoppingList {
id: ID!
name: String!
description: String
items: [ShoppingListLineItem!]!
}
```
In a B2B context, shopping lists are accessed through the business unit:
```graphql
query {
businessUnit(id: "acme-corp") {
shoppingLists(page: 1, pageSize: 10) {
results { id, name, items { variant { sku }, quantity } }
}
}
}
```
## Authentication flow
[Section titled “Authentication flow”](#authentication-flow)
Authentication (login and registration) is identical for B2C and B2B customers. See [Authentication & tokens](/architecture/infrastructure/authentication/) for the full token system. The differentiation happens after login:
1. The user logs in with the standard `customerLogin` mutation
2. The account service loads the user’s business units
3. If business units exist, the first one is selected as the default business context
4. The business context (business unit key + store key) is stored in the session token
5. On subsequent requests, services detect the business context and route operations through the Associate API
The `Customer` type exposes B2B fields for customers with business units:
```graphql
type Customer {
# ... standard fields ...
businessUnit(id: ID): BusinessUnit
businessUnits: [BusinessUnit!]
}
```
The frontend uses `businessUnits` to determine whether to show the B2C or B2B account experience.
# Cloud deployment
> How Evolve deploys to the major clouds (AWS, GCP, Azure) with Terraform and MACH Composer, or runs its whole stack on Vercel.
Evolve supports two deployment paths as first-class targets: the major cloud providers (AWS, GCP, and Azure), and Vercel.
## Vercel
[Section titled “Vercel”](#vercel)
Evolve can run its whole stack on Vercel as one project: the storefront, the GraphQL gateway, and the domain services. Deployment uses Vercel’s own build and tooling, not Terraform or MACH Composer, and it brings preview deployments and other developer-experience features to the whole platform. See [Hosting on Vercel](/architecture/cloud/vercel/).
## Cloud providers (AWS, GCP, and Azure)
[Section titled “Cloud providers (AWS, GCP, and Azure)”](#cloud-providers-aws-gcp-and-azure)
Each project runs on a single cloud provider, but the same backend services deploy to any of the three with only configuration changes. Infrastructure is managed with Terraform, orchestrated by MACH Composer, and deployed through GitHub Actions CI/CD pipelines.
```
graph TD;
subgraph CI/CD
gh["GitHub Actions"]
mcc["MACH Composer Cloud"]
end
subgraph Infrastructure
tf["Terraform"]
mc["MACH Composer"]
end
subgraph Cloud providers
aws["AWS ECS Fargate + Lambda"]
gcp["GCP Cloud Run"]
azure["Azure Container Apps"]
end
subgraph Shared services
cdn["CDN CloudFront / Cloud LB / Front Door"]
registry["Container registry ECR / GAR / ACR"]
cache["Redis / ElastiCache"]
otel["OpenTelemetry"]
end
gh --> mcc
mcc --> mc
mc --> tf
tf --> aws
tf --> gcp
tf --> azure
gh --> registry
aws --> cdn
gcp --> cdn
azure --> cdn
aws --> cache
aws --> otel
gcp --> otel
azure --> otel
```
## Design principles
[Section titled “Design principles”](#design-principles)
Evolve’s backend services follow [twelve-factor app](https://12factor.net/) methodology: configuration through environment variables, stateless processes, structured logging to stdout, and fast startup with graceful shutdown. This is what makes the same container image portable across all supported cloud providers.
## Containers
[Section titled “Containers”](#containers)
Every backend service and the Next.js frontend are packaged as Docker containers. This gives you a consistent deployment unit across all clouds and environments.
In production, containers run on the cloud-native orchestration service for your chosen provider:
* **AWS**: ECS Fargate (serverless containers)
* **GCP**: Cloud Run (serverless containers, scales to zero)
* **Azure**: Container Apps
Serverless runtimes like AWS Lambda can be used alongside containers where appropriate (for example, payment webhook handlers).
### Container registries
[Section titled “Container registries”](#container-registries)
Each cloud uses its own container registry:
| Cloud | Registry |
| ----- | -------------------------------- |
| AWS | ECR (Elastic Container Registry) |
| GCP | GAR (Google Artifact Registry) |
| Azure | ACR (Azure Container Registry) |
The CI/CD pipeline builds images once and pushes them to the registry for the target cloud.
### Local development
[Section titled “Local development”](#local-development)
Evolve includes a `docker-compose.yaml` for local development that provides supporting services:
* **Redis**: caching (port 6379)
* **Jaeger**: distributed tracing UI (port 16686)
* **OpenTelemetry Collector**: trace collection (ports 4317, 4318)
* **Prometheus**: metrics (port 9090)
* **Mailpit**: local SMTP for email testing (ports 1025, 8025)
* **MongoDB**: document storage (port 27017)
This lets you run the full platform locally with an environment that closely resembles production.
## Terraform
[Section titled “Terraform”](#terraform)
All infrastructure is defined as code using Terraform. This covers both cloud resources and [SAAS service configuration](/architecture/cloud/saas-configuration/): commercetools projects, CMS spaces, search indices, and other third-party services are all provisioned through Terraform. A new environment (including all its SAAS dependencies) can be spun up from a single `terraform apply`.
Each service has its own Terraform configuration per cloud provider:
* backend/services/{service-name}/terraform/
* aws/ ECS task, ALB target group, IAM roles
* …
* gcp/ Cloud Run service, NEG, IAM
* …
* azure/ Container App, identity, role assignments
* …
The frontend follows the same pattern, with additional configuration for CDN and static asset hosting:
* frontend/site/terraform/
* aws/ ECS + CloudFront + S3
* …
* gcp/ Cloud Run + Cloud Load Balancing
* …
* azure/ Container Apps + Front Door
* …
Terraform remote state is stored in the cloud provider’s native storage (S3 for AWS, GCS for GCP, Azure Storage for Azure), each with locking to prevent concurrent modifications.
## MACH Composer
[Section titled “MACH Composer”](#mach-composer)
Note
MACH Composer and Terraform apply to the cloud-provider path only. The [Vercel deployment](/architecture/cloud/vercel/) uses Vercel’s own tooling instead. On the cloud-provider path MACH Composer is also optional: since all infrastructure is defined in Terraform, you can manage deployments directly with Terraform and your own CI/CD pipelines if you prefer.
[MACH Composer](https://machcomposer.io) orchestrates the deployment of all components in the platform. It manages the composition of services, their configuration, and the Terraform execution across environments.
A MACH Composer configuration file defines:
* Which cloud provider and region to deploy to
* Which components (services) to include
* How components reference each other (e.g., the checkout service needs the account service endpoint)
* Environment-specific settings (SAAS API keys, feature flags)
- config/
* aws/
* demo.yaml AWS test environment
* gcp/
* demo.yaml GCP test environment
* azure/
* tst.yaml Azure test environment
* prd.yaml Azure production
MACH Composer Cloud (MCC) integrates with the CI/CD pipeline to track component versions and trigger deployments when new versions are available.
## CI/CD
[Section titled “CI/CD”](#cicd)
GitHub Actions handles the build and deployment pipeline. The key workflows:
* **Build**: validates code, builds Docker images, pushes to the container registry, and registers the component version with MACH Composer Cloud
* **Deploy**: triggers MACH Composer to apply the new component versions to the target environment
* **Validate**: checks Terraform configurations and MACH Composer configs on pull requests
Authentication to cloud providers uses OIDC federation (no static credentials):
* **AWS**: GitHub OIDC role assumption
* **GCP**: Workload Identity Federation
* **Azure**: OIDC with service principal
## CDN and frontend hosting
[Section titled “CDN and frontend hosting”](#cdn-and-frontend-hosting)
On the cloud-provider path, the frontend is a Next.js application deployed as a container, with a CDN in front for static assets and caching. Each cloud uses its native CDN service (CloudFront on AWS, Cloud CDN on GCP, Front Door on Azure).
See [cloud services](/architecture/cloud/cloud-services/) for the full service breakdown per cloud provider.
## Choosing a cloud provider
[Section titled “Choosing a cloud provider”](#choosing-a-cloud-provider)
Each project runs on a single cloud provider, chosen based on the organization’s preferences and existing infrastructure. Evolve does not run one instance across several providers at once. Supporting AWS, GCP, and Azure is about choice and portability rather than simultaneous use:
* **No vendor lock-in**: switch providers if requirements change
* **Client flexibility**: different clients can use different clouds
* **Compliance**: deploy in the regions or on the providers required by regulation
The backend services, Terraform modules, and CI/CD pipelines are designed so that adding or switching a cloud target requires only infrastructure configuration, not code changes. Migrating from one provider to another is supported, and a temporary state spanning two providers is possible during such a migration, managed through MACH Composer, though it is uncommon.
[Observability](/architecture/cloud/observability/)How Evolve uses OpenTelemetry for distributed tracing, metrics, and frontend performance monitoring.
[Cloud services](/architecture/cloud/cloud-services/)Detailed breakdown of cloud services used per provider, networking, and CDN configuration.
[SAAS configuration](/architecture/cloud/saas-configuration/)How Evolve uses Terraform to provision and configure SAAS services like commercetools, Contentful, Storyblok, and Algolia.
# Cloud services
> Detailed breakdown of cloud services used per provider, networking, and CDN configuration.
This page details the specific cloud services Evolve uses on each provider. The [cloud deployment overview](/architecture/cloud/) covers the general architecture; this page helps teams understand what gets provisioned when you deploy to a specific cloud.
## AWS
[Section titled “AWS”](#aws)
AWS is the most mature deployment target, with the broadest set of service integrations.
```
graph TD;
internet["Internet"]
cf["CloudFront (CDN)"]
alb_pub["Public ALB"]
alb_int["Internal ALB"]
frontend["Frontend (ECS Fargate)"]
gateway["GraphQL Gateway (ECS Fargate)"]
services["Domain services (ECS Fargate)"]
lambda["Payment webhooks (Lambda)"]
redis["ElastiCache (Redis)"]
s3["S3 (static assets)"]
internet --> cf
cf --> alb_pub
cf --> s3
alb_pub --> frontend
alb_pub --> gateway
gateway --> alb_int
alb_int --> services
alb_int --> lambda
services --> redis
```
### Compute
[Section titled “Compute”](#compute)
* **ECS Fargate**: runs all containerized services (frontend, gateway, domain services). Fargate removes the need to manage EC2 instances; you define CPU and memory per task.
* **Lambda**: used for specific workloads like payment webhook handlers and API extensions, where event-driven invocation is a better fit than a long-running container.
### Networking
[Section titled “Networking”](#networking)
* **Public ALB**: routes external traffic to the frontend and GraphQL gateway. Sits behind CloudFront.
* **Internal ALB**: routes east-west traffic between the gateway and domain services. Not exposed to the internet, keeping service-to- service communication private.
* **API Gateway**: can be used alongside ALB for Lambda-based services that need HTTP API management.
* **Route53**: DNS management for all domains.
### Storage and caching
[Section titled “Storage and caching”](#storage-and-caching)
* **S3**: stores static frontend assets (JS, CSS, images) served through CloudFront.
* **ElastiCache (Redis)**: provides caching for domain services (product data, CMS content, session data).
### CDN
[Section titled “CDN”](#cdn)
* **CloudFront**: sits in front of the entire application. Routes static asset requests to S3 and dynamic requests to the ALB. Supports HTTP/2 and HTTP/3. Origin access control ensures S3 is only accessible through CloudFront.
### Security and configuration
[Section titled “Security and configuration”](#security-and-configuration)
* **IAM**: roles and policies for ECS tasks, Lambda functions, and CI/CD access.
* **KMS**: encryption key management for secrets and sensitive data.
* **SSM Parameter Store**: stores configuration values and secrets referenced by ECS task definitions.
* **ECR**: container image registry.
## GCP
[Section titled “GCP”](#gcp)
GCP uses Cloud Run as its primary compute service, offering automatic scaling (including scale-to-zero) without managing infrastructure.
```
graph TD;
internet["Internet"]
lb["Cloud Load Balancing (Cloud CDN)"]
frontend["Frontend (Cloud Run)"]
gateway["GraphQL Gateway (Cloud Run)"]
services["Domain services (Cloud Run)"]
internet --> lb
lb --> frontend
lb --> gateway
gateway --> services
```
### Compute
[Section titled “Compute”](#compute-1)
* **Cloud Run**: runs all containerized services. Supports automatic scaling based on request volume, including scaling to zero when idle. CPU is allocated only during request processing by default, keeping costs low for lower-traffic services.
### Networking
[Section titled “Networking”](#networking-1)
* **Cloud Load Balancing**: L7 load balancer with Cloud CDN enabled. Uses serverless Network Endpoint Groups (NEGs) to route to Cloud Run services.
* **VPC**: services run within a VPC for network isolation. Cloud Run services connect through VPC connectors for private communication.
* **Cloud DNS**: DNS management.
### CDN
[Section titled “CDN”](#cdn-1)
* **Cloud CDN**: integrated with Cloud Load Balancing. Uses origin headers for cache control. Managed SSL certificates handle HTTPS.
### Security and configuration
[Section titled “Security and configuration”](#security-and-configuration-1)
* **Workload Identity**: GCP’s preferred authentication method. CI/CD pipelines and Cloud Run services use workload identity instead of service account keys.
* **Artifact Registry (GAR)**: container image registry.
* **Secret Manager**: stores sensitive configuration values.
## Azure
[Section titled “Azure”](#azure)
Azure uses Container Apps, which provides a managed container hosting environment with built-in scaling and networking.
```
graph TD;
internet["Internet"]
fd["Front Door (CDN + DDoS)"]
frontend["Frontend (Container Apps)"]
gateway["GraphQL Gateway (Container Apps)"]
services["Domain services (Container Apps)"]
internet --> fd
fd --> frontend
fd --> gateway
gateway --> services
```
### Compute
[Section titled “Compute”](#compute-2)
* **Container Apps**: runs all containerized services within a Container App Environment. Supports configurable min/max replicas and resource allocation (CPU, memory) per service.
### Networking
[Section titled “Networking”](#networking-2)
* **Container App Environment**: provides network isolation for all services in a deployment. Services within the same environment can communicate internally.
* **Front Door**: global CDN and DDoS protection layer that routes external traffic to Container Apps.
### CDN
[Section titled “CDN”](#cdn-2)
* **Front Door**: combines CDN, DDoS protection, and global load balancing in a single service. Routes traffic to the nearest regional deployment.
### Security and configuration
[Section titled “Security and configuration”](#security-and-configuration-2)
* **Managed Identities**: user-assigned identities for workload authentication. Avoids storing credentials in configuration.
* **Role Assignments**: Azure RBAC controls access to resources.
* **Azure Container Registry (ACR)**: container image registry.
## Service comparison
[Section titled “Service comparison”](#service-comparison)
| Concern | AWS | GCP | Azure |
| -------------------- | ----------------------- | -------------------- | ---------------------- |
| Containers | ECS Fargate | Cloud Run | Container Apps |
| Serverless functions | Lambda | Cloud Functions | (not used) |
| Load balancing | ALB (public + internal) | Cloud Load Balancing | Front Door |
| CDN | CloudFront | Cloud CDN | Front Door |
| Container registry | ECR | GAR | ACR |
| Caching | ElastiCache (Redis) | Memorystore | Azure Cache for Redis |
| DNS | Route53 | Cloud DNS | Azure DNS |
| Secrets | SSM + KMS | Secret Manager | Key Vault |
| CI/CD auth | OIDC role assumption | Workload Identity | OIDC service principal |
| Static assets | S3 | Cloud Storage | Blob Storage |
## Networking patterns
[Section titled “Networking patterns”](#networking-patterns)
Regardless of cloud provider, Evolve follows the same networking principles:
* **External traffic** (user requests) enters through the CDN and reaches the frontend and GraphQL gateway through a public load balancer or ingress.
* **Internal traffic** (gateway to domain services) stays within the private network. Services communicate over internal endpoints that are not exposed to the internet.
* **Outbound traffic** to SAAS backends (commercetools, CMS, payment providers) goes directly from domain services over HTTPS. No special egress configuration is needed beyond standard internet access.
This separation ensures that domain services are never directly reachable from the internet, reducing the attack surface of the platform.
# Observability
> How Evolve uses OpenTelemetry for distributed tracing, metrics, and frontend performance monitoring.
Evolve is a distributed system with many moving parts. When something goes wrong, you need to trace a request across services to find the problem. Evolve builds observability into the platform from the start, using OpenTelemetry as the standard instrumentation layer.
## OpenTelemetry
[Section titled “OpenTelemetry”](#opentelemetry)
Every service in Evolve is instrumented with [OpenTelemetry](https://opentelemetry.io/) (OTel). This provides distributed tracing, metrics, and logging through a vendor-agnostic standard. Because OTel is an open standard, you can send telemetry data to any compatible backend.
Evolve has production deployments running on:
* **Honeycomb**: APM and distributed tracing
* **Sentry**: error tracking and performance monitoring
* **Google Cloud Trace**: trace visualization on GCP
* **Azure Monitor**: tracing and metrics on Azure
Other OTel-compatible services (Datadog, New Relic, AWS X-Ray, Grafana, and others) work out of the box.
### How it works
[Section titled “How it works”](#how-it-works)
Each service exports traces and metrics to an OpenTelemetry Collector, which forwards them to your chosen observability backend:
```
graph LR;
service1["Domain service"]
service2["Domain service"]
frontend["Frontend"]
collector["OTel Collector"]
backend["Observability backend (Honeycomb, Sentry, etc.)"]
service1 -- "OTLP" --> collector
service2 -- "OTLP" --> collector
frontend -- "OTLP" --> collector
collector --> backend
```
Services send telemetry using the OTLP protocol (gRPC on port 4317 or HTTP on port 4318). The collector handles batching, sampling, and routing to one or more backends. The Rust-based GraphQL gateway (Hive Router) also exports traces and metrics over OTLP and propagates trace context via B3 headers, matching the rest of the stack.
### Local development
[Section titled “Local development”](#local-development)
For local development, Evolve includes Jaeger and an OpenTelemetry Collector in the `docker-compose.yaml`. Jaeger provides a web UI (port 16686) for viewing traces across services, giving you the same distributed tracing experience locally that you have in production.
## Sentry
[Section titled “Sentry”](#sentry)
While OpenTelemetry handles distributed tracing and metrics, Sentry provides error tracking and performance monitoring. Evolve includes a deep Sentry integration across both frontend and backend, connected to OpenTelemetry through `@sentry/opentelemetry`.
### Observability package
[Section titled “Observability package”](#observability-package)
The `@evolve-framework/core/observability` module wraps Sentry initialization together with OpenTelemetry setup. Every service calls `initObservability()` at startup, which configures both systems with consistent sampling and context propagation:
```typescript
import { initObservability } from "@evolve-framework/core/observability";
initObservability();
```
Under the hood, this:
1. Calls `configureSentry()` with integrations for HTTP, uncaught exceptions, console capture, request data, and frame rewriting
2. Configures OpenTelemetry with a `SentrySampler` so traces are sampled consistently across both systems, and a `SentryContextManager` for trace context propagation
3. Sets up a pino logger stream that forwards error-level logs to Sentry, enriched with logger context (trace IDs, store context). Sensitive fields (authorization headers, cookies, emails, passwords) are redacted before sending.
### Frontend
[Section titled “Frontend”](#frontend)
The Next.js storefront uses `@sentry/nextjs` with three initialization points:
* **Server** (`instrumentation.server.ts`): full tracing with `SentrySampler` for OpenTelemetry integration, and console capture at error/critical levels
* **Edge** (`instrumentation.edge.ts`): minimal initialization with tracing and replays disabled
* **Client** (`sentry.client.config.ts`): reads DSN, release, and environment from `sentry:dsn`, `sentry:release`, and `sentry:environment` meta tags injected by the root layout at runtime
The meta tag approach means the client SDK picks up the correct DSN and environment without exposing values in the JavaScript bundle. The client also extracts the user ID from the session JWT in cookies and sets it on the Sentry scope.
A CSP tunnel route (`/api/capture-errors`) is configured through `withSentryConfig()` so error reports bypass content security policy restrictions and ad blockers. Source maps are uploaded during the build and deleted afterward. CSP violation reports are also sent to Sentry through a Report-To header.
React errors are captured through a custom error boundary wrapping `react-error-boundary` that calls `captureReactException()` for full component stack diagnostics. The boundary accepts a `captureSentry` prop to optionally disable reporting.
### Backend
[Section titled “Backend”](#backend)
Backend services get Sentry integration through `initObservability()`, which every service calls at startup. For Lambda-based services, `lambdaHandlerFactory()` wraps handlers with both Sentry and OpenTelemetry instrumentation and creates spans with Lambda-specific attributes (request ID, function ARN, version).
The pino logger is configured with a Sentry stream destination. Any log at error level or above is forwarded to Sentry as either a `captureException` (for errors) or `captureMessage` (for messages), enriched with the logger’s bindings (trace IDs, store context, service name).
### Configuration
[Section titled “Configuration”](#configuration)
Sentry is configured through the Mach Composer `sentry` plugin. The auth token is stored as a SOPS-encrypted secret, and the organization, project, and rate limits are set as global config:
```yaml
plugins:
sentry:
source: mach-composer/sentry
version: 0.1.3
global:
sentry:
auth_token: ${var.secrets.sentry.auth_token}
organization: "lab-digital"
project: "evolve"
rate_limit_window: 3600
rate_limit_count: 1000
```
Each component that needs Sentry lists it in its `integrations`. The plugin provides the `sentry_dsn` Terraform variable to each component’s module, which is then mapped to environment variables:
| Variable | Purpose |
| -------------------- | --------------------------------------------------- |
| `SENTRY_DSN` | Sentry project DSN. If not set, Sentry is disabled. |
| `SENTRY_ENVIRONMENT` | Environment name (falls back to `ENVIRONMENT`) |
| `SERVICE_NAME` | Service identifier for tagging |
## Core Web Vitals monitoring
[Section titled “Core Web Vitals monitoring”](#core-web-vitals-monitoring)
Because Evolve already exports frontend telemetry through OpenTelemetry, you can extend the instrumentation to capture Core Web Vitals in real time. In e-commerce, these metrics directly impact conversion rates and search engine rankings.
With this approach you get:
* **Per-page metrics**: see which pages have performance issues
* **Release impact**: track how deployments affect performance
* **Instant alerts**: respond to performance regressions immediately instead of waiting for Google Analytics data (which can be delayed up to 48 hours)
Note
While Core Web Vitals can be tracked through Google Analytics or Search Console, the data is delayed by up to 48 hours. For e-commerce, where a performance regression can reduce conversion immediately, real-time monitoring through your own observability stack lets you detect and fix issues before they impact revenue.
## Further reading
[Section titled “Further reading”](#further-reading)
Honeycomb has published a detailed guide on implementing Core Web Vitals monitoring with OpenTelemetry:
* [Observing Core Web Vitals with OpenTelemetry: Part One](https://www.honeycomb.io/blog/core-web-vitals-opentelemetry)
* [Observing Core Web Vitals with OpenTelemetry: Part Two](https://www.honeycomb.io/blog/core-web-vitals-opentelemetry-part2)
# SAAS configuration
> How Evolve uses Terraform to provision and configure SAAS services like commercetools, Contentful, Storyblok, and Algolia.
Evolve manages SAAS service configuration through Terraform, the same infrastructure-as-code tool used for cloud resources. This means commercetools projects, CMS content models, search indices, and observability tools are all defined declaratively, version-controlled, and reproducible across environments.
This approach may be unfamiliar if you’re used to configuring SAAS services through their web interfaces. The benefit is significant: spinning up a new environment (staging, demo, new client) is a single `terraform apply` that provisions everything, from cloud infrastructure to SAAS configurations, in one go. No manual clicking through admin panels, no forgotten settings, no configuration drift between environments.
## What gets configured
[Section titled “What gets configured”](#what-gets-configured)
Evolve includes Terraform configurations for all integrated SAAS services:
* backend/configuration/
* commercetools/ Project settings, stores, channels,
* …
* │ product types, tax categories
* contentful/ Locales, content types, editor interfaces
* …
* storyblok/ Components, component groups
* …
* algolia/ Indices, virtual indices, facet config
* …
Each directory contains a self-contained Terraform module that can be applied independently or orchestrated through MACH Composer.
## Commerce configuration
[Section titled “Commerce configuration”](#commerce-configuration)
The commercetools Terraform provider manages the commerce backend: project settings, stores, channels, product types, and tax categories.
```hcl
resource "commercetools_project_settings" "project" {
name = "my-project"
countries = ["NL", "DE", "GB"]
currencies = ["EUR", "GBP"]
languages = ["nl-NL", "en-GB", "de-DE"]
messages {
enabled = true
}
carts {
country_tax_rate_fallback_enabled = true
delete_days_after_last_modification = 90
}
}
resource "commercetools_store" "primary" {
key = "my-store"
name = { nl-NL = "My Store" }
languages = commercetools_project_settings.project.languages
countries = commercetools_project_settings.project.countries
distribution_channels = [
commercetools_channel.store.key
]
}
```
Product types, which define the attributes available on products, are also managed through Terraform. This ensures the data model is consistent across environments:
```hcl
resource "commercetools_product_type" "fashion" {
key = "fashion"
name = "Fashion"
description = "Fashion product type"
attribute {
name = "brand"
label = { en-GB = "Brand", nl-NL = "Merk" }
constraint = "None"
type = { name = "text" }
searchable = true
}
}
```
## CMS configuration
[Section titled “CMS configuration”](#cms-configuration)
Both Contentful and Storyblok content models are defined in Terraform. This means the content types and components that content editors work with are version-controlled and consistent across environments.
### Contentful
[Section titled “Contentful”](#contentful)
```hcl
resource "contentful_contenttype" "hero" {
space_id = var.contentful_space_id
environment = var.contentful_environment
id = "hero"
name = "Hero"
display_field = "title"
fields = [
{
id = "title"
name = "Title"
type = "Text"
},
{
id = "image"
name = "Image"
type = "Link"
link_type = "Asset"
},
]
}
```
### Storyblok
[Section titled “Storyblok”](#storyblok)
```hcl
resource "storyblok_component" "hero" {
name = "hero"
space_id = var.storyblok_space_id
is_root = false
is_nestable = true
schema = {
title = {
position = 0
translatable = true
display_name = "Title"
required = true
type = "text"
}
image = {
position = 5
translatable = true
display_name = "Image"
required = true
type = "asset"
filetypes = ["images"]
}
}
}
```
## Search configuration
[Section titled “Search configuration”](#search-configuration)
Algolia indices, including facet configuration, searchable attributes, and sort order replicas, are managed through Terraform:
```hcl
resource "algolia_index" "default" {
for_each = local.store_keys
name = "${each.value}_default"
attributes_config {
searchable_attributes = [
"name",
"description",
"variants.sku",
]
attributes_for_faceting = [
"startingPrice.centAmount",
"hierarchicalCategories.lvl0",
"variants.attributes.brand",
"variants.attributes.color.nl-NL",
"variants.attributes.size",
]
}
}
resource "algolia_virtual_index" "price_asc" {
for_each = local.store_keys
name = "${each.value}_price_asc"
primary_index_name = algolia_index.default[each.key].name
ranking_config {
custom_ranking = ["asc(startingPrice.centAmount)"]
}
}
```
## Terraform providers
[Section titled “Terraform providers”](#terraform-providers)
Evolve uses community-maintained Terraform providers for each SAAS service:
| Service | Provider | Source |
| ------------- | -------------------- | ---------------------------------------------------------------------- |
| commercetools | `labd/commercetools` | [Registry](https://registry.terraform.io/providers/labd/commercetools) |
| Contentful | `labd/contentful` | [Registry](https://registry.terraform.io/providers/labd/contentful) |
| Storyblok | `labd/storyblok` | [Registry](https://registry.terraform.io/providers/labd/storyblok) |
| Algolia | `k-yomo/algolia` | [Registry](https://registry.terraform.io/providers/k-yomo/algolia) |
## MACH Composer integration
[Section titled “MACH Composer integration”](#mach-composer-integration)
MACH Composer ties SAAS configuration into the deployment pipeline. Each SAAS service is registered as a plugin, and components declare which integrations they need:
```yaml
mach_composer:
plugins:
commercetools:
source: mach-composer/commercetools
version: 0.2.1
storyblok:
source: mach-composer/storyblok
version: 0.0.3
algolia:
source: mach-composer/algolia
version: 0.1.0
sentry:
source: mach-composer/sentry
version: 0.1.3
```
SAAS credentials and project-specific settings are defined per environment. MACH Composer injects these as Terraform variables when applying configurations:
```yaml
sites:
- identifier: my-project
commercetools:
project_key: my-project
client_id: ${var.secrets.commercetools.client_id}
client_secret: ${var.secrets.commercetools.client_secret}
storyblok:
space_id: 233248
token: ${var.secrets.storyblok.management_token}
algolia:
app_id: YWHL59LKQC
api_key: ${var.secrets.algolia.write_api_key}
```
Components reference their SAAS integrations, and MACH Composer ensures the right credentials and configurations are passed to each Terraform module:
```yaml
components:
- name: commercetools-config
source: ../../backend/configuration/commercetools/
integrations:
- commercetools
- name: storyblok-config
source: ../../backend/configuration/storyblok/
integrations:
- commercetools
- storyblok
- name: algolia-config
source: ../../backend/configuration/algolia/
integrations:
- algolia
```
This means adding a new environment is a matter of creating a new MACH Composer configuration file with the right credentials. The Terraform modules handle the rest.
# Hosting on Vercel
> Run the entire Evolve stack on Vercel, the storefront, GraphQL gateway, and domain services, as one project with preview deployments and other Vercel developer-experience features.
Evolve can run its entire stack on Vercel: the Next.js storefront, the GraphQL gateway, and the domain services, all in a single Vercel project. This is one of Evolve’s supported deployment targets, alongside the [multi-cloud deployment](/architecture/cloud/) on AWS, GCP, and Azure, and it brings Vercel’s developer experience to the whole platform rather than the storefront alone.
## Architecture
[Section titled “Architecture”](#architecture)
On Vercel the stack maps to Vercel Services within one project:
* **Storefront:** the [Next.js frontend](/architecture/frontend/) runs natively on Vercel.
* **Domain services:** the [domain services](/architecture/core/design/) such as catalog, checkout, account, order, content, and quotes run as Vercel Functions on Fluid Compute.
* **GraphQL gateway:** the [Federation gateway](/architecture/infrastructure/gateway/) composes the domain services into one [supergraph](/architecture/core/graphql/).
Services reach each other over private Vercel service bindings. Only the storefront and the `/graphql` endpoint are exposed publicly through rewrites; the domain services are private by default and reachable only through the gateway.
```
graph TD;
client["Browser, apps, AI agents"]-->site["Storefront (Next.js)"]
client-->gateway["GraphQL gateway (/graphql)"]
site-->gateway
gateway-->account["account"]
gateway-->catalog["catalog"]
gateway-->checkout["checkout"]
gateway-->order["order"]
gateway-->content["content"]
gateway-->quotes["quotes"]
```
## What Vercel brings
[Section titled “What Vercel brings”](#what-vercel-brings)
Because the whole stack lives in one Vercel project, the platform inherits Vercel’s workflow:
* **Preview deployments:** every pull request gets its own URL running the full stack, storefront, gateway, and services, wired to itself, so a change can be reviewed end to end in isolation.
* **Instant rollbacks:** deployments are atomic and immutable, so reverting to a previous version is instant.
* **Region pinning:** compute runs in the region closest to your data.
* **Fluid Compute:** the domain services scale to zero when idle and reuse instances across requests.
* **Environment management:** configuration and secrets are managed through the Vercel CLI and dashboard.
* **Observability:** per-service logs are filterable in the dashboard, and traces are exported through [OpenTelemetry](/architecture/cloud/observability/).
## Deploying
[Section titled “Deploying”](#deploying)
The whole stack deploys with a single command. See [Deploying to Vercel](/how-to/deploying-to-vercel/) for the one-time setup and the deploy steps.
## Alongside multi-cloud
[Section titled “Alongside multi-cloud”](#alongside-multi-cloud)
Vercel is additive. The Terraform-based [multi-cloud path](/architecture/cloud/) on AWS, GCP, and Azure remains fully supported, and a service can target both. Choosing Vercel or multi-cloud is a deployment decision, not a change to the application code.
# Core architecture
> Core architectural concepts: domain services, data model, GraphQL Federation, and the framework that ties them together.
This section covers the foundational building blocks of the Evolve platform: how services are organized, how data flows through the system, and how the framework provides shared patterns for every service.
[Domain services](/architecture/core/design/)How Evolve organizes backend services around business domains and the customer journey.
[Data model](/architecture/core/datamodel/)Evolve's vendor-independent GraphQL data model, inspired by schema.org, and how to extend it.
[GraphQL Federation](/architecture/core/graphql/)GraphQL Federation is the API integration technology in Evolve.
[Evolve Framework](/architecture/core/framework/)Shared backend logic distributed as versioned @evolve-framework/\* packages for painless upgrades and less code to maintain.
[Multi-store support](/architecture/core/store-context/)How Evolve supports multiple stores, locales, and currencies through the StoreContext pattern.
[Page model](/architecture/core/pages/)How Evolve structures pages across CMS and catalog services using GraphQL Federation.
# Commerce engines
> How Evolve integrates commerce engines through domain-service adapters, which engines are supported, and how that keeps engines swappable.
The commerce engine is the system of record for products, carts, orders, and customers. In Evolve it is not the platform: it is one of the SAAS backends that the [domain services](/architecture/core/design/) integrate with, behind the [GraphQL Federation](/architecture/core/graphql/) layer. Every engine is integrated through an adapter, which is what lets Evolve support more than one engine and swap between them.
## An engine-agnostic model
[Section titled “An engine-agnostic model”](#an-engine-agnostic-model)
Evolve defines its [own data model](/architecture/core/datamodel/): a shared GraphQL schema per domain (catalog, cart, checkout, customer, order, and so on) that is independent of any backend. A commerce-engine adapter implements that schema for a specific engine.
Each adapter provides the same set of domain modules built on the same schema: catalog, cart, checkout, account, order, payment, and the B2B modules for business units and quotes. What differs per engine is only the resolver layer: how each field maps to that engine’s own APIs and data. Because the schema and its Federation contract are identical across engines, the storefront, mobile apps, and AI agents that consume the API do not change when the engine changes.
This is the same adapter pattern Evolve uses for [search](/architecture/infrastructure/search/), CMS, and [payment providers](/architecture/payment/).
## Supported engines
[Section titled “Supported engines”](#supported-engines)
Status follows the same categories as the [MACH integrations overview](/architecture/integrations/): **Full** is implemented and in production, **Beta** is usable in the framework but still has gaps, **In development** means a working implementation exists and is being ported into the framework, and **Roadmap** is planned.
| Engine | Status | Coverage |
| ----------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **commercetools** | Full | The reference engine. All domains, including B2B (business units and quotes), search through Algolia or commercetools, and dedicated payment-provider integrations. |
| **Salesforce Commerce Cloud** | Beta | Catalog, cart, checkout, account, orders, and promotions, with search served by Salesforce Commerce Cloud. B2B quoting is not yet implemented and Algolia search is not wired for this engine. A working example ships with the framework. |
| **SAP Commerce Cloud** | In development | A working proof of concept covering the core commerce domains (catalog, checkout, order) and Algolia search is being ported into the framework. |
Other engines are integrated at varying levels, including Commerce Layer, Big Commerce, Emporix, and Shopify Plus. For the status of every engine and SAAS integration, see the [MACH integrations overview](/architecture/integrations/).
## Adding or changing an engine
[Section titled “Adding or changing an engine”](#adding-or-changing-an-engine)
Adding support for a new engine means implementing the domain modules against the shared schema for that engine’s APIs. This is the same work as [adding a search engine](/how-to/adding-a-search-engine/) or another SAAS integration: the contract is fixed by the schema, so the adapter only supplies resolvers and mappers.
Because an engine is an adapter behind a shared, federated schema, the choice of engine is not permanent. Domains can resolve from a different engine without changing the API that consumers depend on, which makes a gradual transition from one engine to another possible. See [Modernizing an existing stack](/architecture/modernization/) for how that fits into adopting Evolve on top of an engine you already run.
# Data model
> Evolve's vendor-independent GraphQL data model, inspired by schema.org, and how to extend it.
Evolve defines its own GraphQL data model that is independent of any backing SAAS service. This is a deliberate design choice: by owning the data model, Evolve can integrate any commerce engine, CMS, or search backend behind the same schema. Clients consume a single, consistent API regardless of which vendors are running underneath.
## Vendor independence
[Section titled “Vendor independence”](#vendor-independence)
Each SAAS backend has its own data model (commercetools has `ProductProjection`, Contentful has `Entry`, Algolia has its own hit format). Evolve does not expose these directly. Instead, each [domain service](/architecture/core/design/) maps vendor-specific data into Evolve’s own GraphQL types. This means:
* **Swapping vendors** does not change the API. Replacing your CMS or search engine requires a new service implementation, not frontend changes.
* **Combining vendors** is straightforward. A single query can return product data from the commerce engine and content from the CMS, merged by [GraphQL Federation](/architecture/core/graphql/).
* **Custom data models** are yours to define. You design the schema that fits your business, not the one your vendor prescribes.
## Schema.org alignment
[Section titled “Schema.org alignment”](#schemaorg-alignment)
Evolve’s data model follows [schema.org](https://schema.org/) conventions where applicable. Schema.org provides a shared vocabulary for structured data on the web, and aligning with it gives practical benefits:
* Familiar naming for developers (e.g., `Product`, `Offer`, `Brand`)
* Easier mapping to structured data for SEO (JSON-LD)
* A well-documented reference for what fields mean
This is not a strict implementation of schema.org; the data model is shaped for e-commerce use cases and GraphQL conventions. But where schema.org provides a sensible name or structure, Evolve follows it.
## Key types
[Section titled “Key types”](#key-types)
The data model spans all [domain services](/architecture/core/design/). Here are the central types across the main domains:
### Products and catalog
[Section titled “Products and catalog”](#products-and-catalog)
```graphql
type Product {
name: String!
slug: String!
brand: String
meta: PageMeta
breadcrumbs: [Breadcrumb!]
variant(sku: String): ProductVariant
variants: [ProductVariant!]!
}
type ProductVariant {
sku: String!
name: String
description: String
price: ProductPrice
availability: ProductAvailability
images: [ProductImage!]
attributes: [ProductAttribute!]
}
type ProductCategory {
name: String!
slug: String!
children: [ProductCategory!]
parent: ProductCategory
}
```
### Cart and checkout
[Section titled “Cart and checkout”](#cart-and-checkout)
```graphql
type Cart {
lineItems: [CartLineItem!]!
totalPrice: Money!
shippingAddress: Address
billingAddress: Address
availableShippingMethods: [ShippingMethod!]
availablePaymentMethods: [PaymentMethod!]
discountCodes: [DiscountCode!]
}
```
### Customer and account
[Section titled “Customer and account”](#customer-and-account)
```graphql
type Customer {
email: EmailAddress!
givenName: String
familyName: String
addresses: AddressConnection!
businessUnits: [BusinessUnit!]
}
```
### Orders
[Section titled “Orders”](#orders)
```graphql
type Order {
orderNumber: String!
status: OrderStatus!
lineItems: [OrderLineItem!]!
totalPrice: Money!
createdAt: DateTime!
}
```
Note
These are simplified examples. The full schema includes many more fields, input types, and connections. You can browse the complete schema interactively using the [schema viewer](https://docs.evolve.labdigital.nl/graphql-schema/).
[](https://docs.evolve.labdigital.nl/graphql-schema/)
## Shared types
[Section titled “Shared types”](#shared-types)
Several types are shared across services using the `@shareable` federation directive. This means multiple services can resolve the same type independently:
* **`Money`**: represents a monetary value with `centAmount` and `currencyCode`
* **`Address`**: postal address used by checkout, account, and order services
* **`PageMeta`**: SEO metadata (title, description, open graph) used by CMS and catalog services
* **`Breadcrumb`**: navigation breadcrumbs resolved by both CMS and catalog
See [page model](/architecture/core/pages/) for details on how page-related types work across services.
## Extending the data model
[Section titled “Extending the data model”](#extending-the-data-model)
Evolve’s data model is a foundation, not a ceiling. Because the architecture uses GraphQL Federation, you can extend it in several ways:
* **Add fields to existing types**: create a new subgraph that extends an Evolve type with additional fields (e.g., adding a `loyaltyPoints` field to `Customer`)
* **Add new types**: introduce entirely new types and queries in a custom subgraph (e.g., a `WishList` or `StoreLocator` type)
* **Override resolvers**: replace how an existing field is resolved without changing the schema
```graphql
# Example: extending Customer with loyalty points
# in a custom subgraph
type Customer @extends @key(fields: "id") {
id: ID! @external
loyaltyPoints: Int!
loyaltyTier: LoyaltyTier!
}
```
The gateway automatically merges your extensions into the supergraph. See [customization and extensibility](/architecture/core/framework/customization/) for detailed patterns and code examples.
# Domain services
> How Evolve organizes backend services around business domains and the customer journey.
Evolve’s backend is split into domain services, each focused on a specific area of the commerce experience. These services integrate with SAAS backends (commerce engines, CMS platforms, search engines, payment providers) and expose a unified GraphQL API through [GraphQL Federation](/architecture/core/graphql/).
```
graph TD;
subgraph "Pre-purchase"
catalog["Catalog"]
cms["CMS"]
end
subgraph "Purchase"
checkout["Checkout"]
payment["Payment"]
end
subgraph "Account"
account["Account"]
bu["Business units"]
end
subgraph "Post-purchase"
order["Orders"]
quotes["Quotes"]
end
gateway["GraphQL Gateway"]
gateway --> catalog
gateway --> cms
gateway --> checkout
gateway --> payment
gateway --> account
gateway --> bu
gateway --> order
gateway --> quotes
```
## Service overview
[Section titled “Service overview”](#service-overview)
### Catalog
[Section titled “Catalog”](#catalog)
Owns the product catalog: product search, product detail, categories, and product listing configuration. Integrates with the commerce backend for product data and optionally with a dedicated [search engine](/architecture/infrastructure/search/) (Algolia) for search and faceting.
### CMS
[Section titled “CMS”](#cms)
Integrates the content management system into the platform. Owns content pages, catalog page content, site layout (header, footer, navigation), and reusable content snippets. Evolve supports both Contentful and Storyblok as CMS backends, with interchangeable implementations behind the same GraphQL schema.
### Checkout
[Section titled “Checkout”](#checkout)
Manages the shopping cart and the checkout process. Handles line item operations (add, update, remove), discount codes, shipping and payment method selection, address validation, and order creation. Orchestrates payment flows by delegating to the appropriate payment service.
### Account
[Section titled “Account”](#account)
Covers customer identity and account management: registration, login, password management, address book, and session handling. Also owns B2B-specific account features like business unit management, associate invitations, roles, and shopping lists. Issues and manages [authentication tokens](/architecture/infrastructure/authentication/) used by all other services.
### Orders
[Section titled “Orders”](#orders)
Handles post-purchase order management: order history, order detail, and order status. Operates in both B2C (scoped to the customer) and B2B (scoped to the business unit) contexts.
### Quotes
[Section titled “Quotes”](#quotes)
Manages the B2B quote workflow: creating quote requests from carts, tracking quote status, and converting accepted quotes into orders. See [B2B & B2C architecture](/architecture/b2b/) for the full quote flow.
### Payment
[Section titled “Payment”](#payment)
Payment processing is split into separate services per payment provider. Each payment service handles payment creation, webhook processing, and transaction management for its provider:
| Service | Provider |
| -------------------------------- | ------------------------ |
| `payment-commercetools-stripe` | Stripe |
| `payment-commercetools-adyen` | Adyen |
| `payment-commercetools-mollie` | Mollie |
| `payment-commercetools-buckaroo` | Buckaroo |
| `payment-commercetools-paynl` | PayNL |
| `payment-commercetools-invoice` | Invoice (pay on account) |
Payment services expose REST endpoints (for webhooks and payment creation) rather than GraphQL, since payment providers communicate through callbacks and redirects. See [Payment architecture](/architecture/payment/) for the full flow and [REST endpoints & webhooks](/architecture/infrastructure/rest-webhooks/) for the webhook contract.
### Email
[Section titled “Email”](#email)
Sends transactional emails (order confirmations, password resets, account invitations) using React-based email templates. Integrates with any SMTP-compatible email service. See [Email & notifications](/architecture/infrastructure/email/) for the full architecture.
### GraphQL gateway
[Section titled “GraphQL gateway”](#graphql-gateway)
The gateway is not a domain service itself but the entry point for all clients. It is a Rust-based Hive Router that composes the subgraphs from all domain services into a single supergraph using [Federation v2](/architecture/core/graphql/). It handles query planning, request routing, and response merging.
## Supporting services
[Section titled “Supporting services”](#supporting-services)
Beyond the core domain services, Evolve includes supporting services for infrastructure concerns:
* **Event router**: routes [events](/architecture/infrastructure/messaging/) between services (e.g., order created events triggering email notifications)
* **PIM integration**: synchronizes product data from external PIM systems (e.g., Bluestone PIM) into the commerce backend
* **Product feed**: generates product feeds for external channels (Google Shopping, marketplaces)
* **Monitoring**: service health monitoring and alerting
## Team organization
[Section titled “Team organization”](#team-organization)
The domain-driven service structure maps naturally to team organization. Services group along the customer journey, allowing teams to be structured around business areas:
| Area | Services |
| ------------- | ----------------------- |
| Pre-purchase | Catalog, CMS |
| Purchase | Checkout, Payment |
| Account | Account (including B2B) |
| Post-purchase | Orders, Quotes |
Small teams can own the entire platform. Larger organizations can assign services to dedicated squads, using tools like GitHub’s `CODEOWNERS` to enforce ownership boundaries. Because services communicate only through GraphQL Federation, teams can work independently without blocking each other.
# Evolve Framework
> Shared backend logic distributed as versioned @evolve-framework/* packages for painless upgrades and less code to maintain.
The Evolve Framework contains the shared backend logic of the platform: GraphQL modules, resolvers, dataloaders, mappers, and the server runtime they plug into. It lives in its own repository (`evolve-framework`) and is published as `@evolve-framework/*` packages to the Evolve registry (`npm.registry.evolve-platform.com`). Implementations depend on it like any other library.
## Why
[Section titled “Why”](#why)
Every Evolve backend service used to ship with a complete copy of its GraphQL resolvers, schema definitions, dataloaders, mappers, and test infrastructure. A typical project contained hundreds of files and thousands of lines of code that was essentially identical across deployments. This created several problems:
* **Upgrade friction**: pulling in platform improvements meant manually diffing and merging hundreds of files
* **Multiplied maintenance**: a bug fix needed to be applied once per project instead of once
* **Unclear boundaries**: it was hard to tell which code was platform baseline and which was project-specific
## Packages
[Section titled “Packages”](#packages)
The framework repository contains 19 packages under `packages/`, all published under the `@evolve-framework` scope.
### Foundation and tooling
[Section titled “Foundation and tooling”](#foundation-and-tooling)
| Package | Description |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core` | The module system (`Module`, `AbstractModule`, `CompositeModule`), the `DomainService` runtime, cache, messaging, storage, HTTP client, and observability |
| `schemas` | GraphQL type definitions for every commerce domain, consumed by the implementation packages |
| `json-schema-to-zod` | Converts JSON Schema definitions into Zod validators |
| `packager` | Packages services into deployable artifacts (Docker, AWS Lambda, Azure Functions) |
### Commercetools
[Section titled “Commercetools”](#commercetools)
| Package | Description |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `commercetools` | GraphQL modules, resolvers, dataloaders, mappers, factories, and test tooling for commercetools |
| `commercetools-adyen`, `commercetools-buckaroo`, `commercetools-invoice`, `commercetools-mollie`, `commercetools-paynl`, `commercetools-stripe` | Payment provider modules for the commercetools checkout, one package per provider |
### CMS
[Section titled “CMS”](#cms)
| Package | Description |
| ------------ | --------------------------------------------------------------------------------------- |
| `contentful` | `ContentfulModule` with resolvers, a customizable mapper registry, and webhook handling |
| `storyblok` | `StoryblokModule`, interchangeable with the Contentful package at the subgraph level |
### Other integrations
[Section titled “Other integrations”](#other-integrations)
| Package | Description |
| --------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `sfcc` | Salesforce Commerce Cloud implementation modules (early stage) |
| `cloud-adapter-aws`, `cloud-adapter-azure`, `cloud-adapter-gcp` | Cloud-specific bindings for messaging and storage |
| `mcp-core` | Building blocks for MCP servers that expose GraphQL operations as AI tools |
| `user-notifications` | Queued user notification client and message contracts |
## How implementations consume it
[Section titled “How implementations consume it”](#how-implementations-consume-it)
Implementations point the `@evolve-framework` scope at the Evolve registry in `.npmrc` and pin versions once, in the `catalog:` section of `pnpm-workspace.yaml`. Individual `package.json` files use `catalog:` as the version, so a framework upgrade is a one-line change.
New versions are published to the registry regularly; alongside stable releases, snapshot versions (`0.0.0--`, npm tag `snapshot`) are published continuously, so unreleased work can be tested in an implementation before it is formally released. You consume both the same way: pin the version in your `catalog:`.
## Framework versus implementation
[Section titled “Framework versus implementation”](#framework-versus-implementation)
The framework owns everything that is the same across projects: modules, resolvers, schemas, dataloaders, mappers, and the server runtime. Your implementation owns everything that is specific to your project: configuration loading, module composition, context wiring, custom REST routes, event handlers, and any resolvers or mappers you override or add. See [Customization and extensibility](/architecture/core/framework/customization/) for the extension points.
## Frequently asked questions
[Section titled “Frequently asked questions”](#frequently-asked-questions)
**Does the GraphQL API change when I upgrade?** Not silently. The schema each service exposes is defined by the modules it composes; schema changes arrive through explicit version bumps and are documented in the package changelogs.
**Can I stay on an older framework version?** Yes. Framework packages are versioned and pinned in your `catalog:`. You upgrade on your own schedule.
**When should I use a snapshot version?** Only to validate unreleased framework changes against your implementation. Production deployments should pin released versions.
## What’s next
[Section titled “What’s next”](#whats-next)
[Architecture](/architecture/core/framework/architecture/)The module system (Module, AbstractModule, CompositeModule), the DomainService runtime, and the anatomy of a service.
[Composition & deployment topologies](/architecture/core/framework/composition/)Why the framework is built around composable modules: choose your own service topology, from a single backend service to one service per domain.
[Customization and extensibility](/architecture/core/framework/customization/)How to override resolvers, extend schemas, add project-specific modules, and register custom CMS content-type mappers.
[Migration guide](/architecture/core/framework/migration-guide/)Step-by-step guide for migrating an implementation from the vendored in-repo framework packages to registry-published versions.
[Roadmap](/architecture/core/framework/roadmap/)What the framework has delivered — extraction, registry publishing, implementation packages, gateway — and what comes next.
# Architecture
> The module system (Module, AbstractModule, CompositeModule), the DomainService runtime, and the anatomy of a service.
The framework organizes shared backend code into layered packages. Services compose modules from these packages into a `DomainService` and add only project-specific logic.
## Package layering
[Section titled “Package layering”](#package-layering)
```
graph TD;
impl["Implementation packages commercetools, contentful, storyblok, sfcc, ..."]
schemas["@evolve-framework/schemas GraphQL type definitions for all domains"]
core["@evolve-framework/core Module system, DomainService, cache, messaging"]
impl --> schemas
impl --> core
schemas --> core
```
* **Core** (`@evolve-framework/core`): the module system (`Module`, `AbstractModule`, `CompositeModule`), the `DomainService` runtime, cache, messaging, storage, HTTP client, and observability. Vendor-agnostic.
* **Schemas** (`@evolve-framework/schemas`): GraphQL type definitions, organized by domain (cart, catalog, customer, order, and so on). Vendor-agnostic.
* **Implementation packages**: the concrete implementation for a specific SAAS integration — resolvers, data mappers, dataloaders, and the modules that wire everything together. `@evolve-framework/commercetools` is the largest; `@evolve-framework/contentful`, `@evolve-framework/storyblok`, the payment provider packages, and `@evolve-framework/sfcc` follow the same pattern on the same core and schemas.
## The module system
[Section titled “The module system”](#the-module-system)
A module encapsulates one domain: its GraphQL type definitions and resolvers, and optionally HTTP routes (webhooks, REST endpoints). The contract is the `Module` interface from `@evolve-framework/core`:
```typescript
export interface Module {
getGraphQLConfig(): GraphQLConfig | undefined;
getGraphQLSchema(): string | undefined;
getHttpConfig(): HttpConfig | undefined;
init?(): Promise | void;
close?(): Promise | void;
}
export type GraphQLConfig = {
resolvers: GraphQLResolverMap;
typeDefs: DocumentNode;
};
export type HttpConfig = {
routes: (app: FastifyInstance) => void;
plugins?: Array<[FastifyPluginCallback, FastifyPluginOptions]>;
};
```
Two lifecycle rules matter:
* **Constructors stay cheap and side-effect-free.** Codegen tooling instantiates modules and calls `getGraphQLSchema()` without runtime config or network access.
* **`init()` is the place for side-effecty setup** — building SDK clients, registering module-scoped singletons. `DomainService.init()` calls it once before consuming `getGraphQLConfig()` and `getHttpConfig()`. `close()` releases those resources on shutdown.
### AbstractModule
[Section titled “AbstractModule”](#abstractmodule)
`AbstractModule` is the standard base class: assign `typedefs` (a `DocumentNode`) and `resolvers`, and the interface methods are implemented for you. This is the real `QuoteModule` from `@evolve-framework/commercetools`:
```typescript
import { AbstractModule } from "@evolve-framework/core";
import getTypeDefs from "@evolve-framework/schemas/graphql/quote";
export class QuoteModule extends AbstractModule {
resolvers: Resolvers = {
Mutation: {
quoteRequestCreate: quoteRequestCreateMutation,
},
BusinessUnit: {
quoteRequest: quoteRequestResolver,
quoteRequests: quoteRequestsResolver,
quote: quoteResolver,
quotes: quotesResolver,
},
Quote: quoteFieldResolvers,
};
typedefs = getTypeDefs();
}
```
Type definitions come from `@evolve-framework/schemas/graphql/`, which exports a `getTypeDefs()` function per domain (some accept options, for example `getTypeDefs({ includeB2B: true })` for the customer domain).
### Module configuration
[Section titled “Module configuration”](#module-configuration)
Modules that need configuration take a typed options object in their constructor. There is no generic `config` bag — each module declares exactly what it needs:
```typescript
import {
CatalogModule,
CustomerModule,
} from "@evolve-framework/commercetools";
new CatalogModule({ clientFactory });
new CustomerModule({
config: {
CTP_PROJECT_KEY: config.CTP_PROJECT_KEY,
CTP_AUTH_URL: config.CTP_AUTH_URL,
CTP_USER_CLIENT_ID: config.CTP_USER_CLIENT_ID,
CTP_USER_CLIENT_SECRET: config.CTP_USER_CLIENT_SECRET,
},
});
```
Modules with routes implement `getHttpConfig()`. The `ContentfulModule`, for example, registers its change-notification webhook this way, so the route is mounted automatically when the module is composed into a service.
### CompositeModule
[Section titled “CompositeModule”](#compositemodule)
Why composition is the framework’s central design choice — and how it lets you pick a service topology from one backend service to one per domain — is covered in [Composition & deployment topologies](/architecture/core/framework/composition/).
`CompositeModule` composes multiple modules into one. It implements `Module` itself, so a composite is what you hand to `DomainService`:
```typescript
import { QuoteModule } from "@evolve-framework/commercetools";
import { CompositeModule } from "@evolve-framework/core";
export const createModule = (): CompositeModule => {
return new CompositeModule([new QuoteModule()]);
};
```
Composition semantics:
* **Type definitions** are merged with conflict detection (`throwOnConflict`): two modules defining the same type differently is a startup error, not a silent override. Merging is federation-aware — it reconciles Federation directives (`@external`, `@extends`, `@requires`, `@provides`) that would otherwise be invalid when modules designed as separate subgraphs are composed into a single subgraph.
* **Resolvers** are merged per GraphQL type name; when two modules resolve the same field, the later module in the array wins.
* **HTTP config** is concatenated: every module’s routes and Fastify plugins are registered.
* **`init()` and `close()`** are chained across all child modules, in order.
**Entity hydration at module boundaries.** When modules that were designed as separate Federation subgraphs run inside one composite, one module may hand another an entity stub — for example the CMS module returning `{ __typename: "ProductVariant", sku }` for the catalog module to enrich. A real gateway would call the owning subgraph’s `__resolveReference`; inside a composite there is no gateway, so `CompositeModule` wraps each entity type’s field resolvers to pipe the parent through the owning module’s `__resolveReference` first. The hydrated parent is cached per-request, so `__resolveReference` runs at most once per stub. Owning modules must keep `__resolveReference` idempotent: if the parent is already hydrated, return it as-is.
### Available modules
[Section titled “Available modules”](#available-modules)
`@evolve-framework/commercetools` ships the following modules:
| Module | Domain |
| -------------------- | ------------------------------------------------------------------------------------------------- |
| `AuthModule` | Sessions and authentication — login, logout, guest sessions, password reset, token REST endpoints |
| `CustomerModule` | Customer profile — customer queries, address CRUD, account updates |
| `CartModule` | Shopping cart — line items, discount codes, cart updates |
| `CatalogModule` | Product catalog — products, categories, product search (Algolia or commercetools) |
| `CheckoutModule` | Checkout completion |
| `OrderModule` | Order history and details |
| `PaymentModule` | Payment creation and payment methods |
| `BusinessUnitModule` | B2B business units, associates, and roles |
| `QuoteModule` | B2B quotes and quote requests |
| `ShoppingListModule` | Shopping list CRUD |
The CMS packages ship `ContentfulModule` and `StoryblokModule`, the payment provider packages ship one module each (for example `MolliePaymentModule`), and `@evolve-framework/sfcc` mirrors the commercetools module set for Salesforce Commerce Cloud.
## DomainService
[Section titled “DomainService”](#domainservice)
`DomainService` from `@evolve-framework/core` composes a module with its HTTP and GraphQL surface and runs it. The lifecycle has three steps:
1. `new DomainService(options)` — cheap, no side effects
2. `await service.init()` — runs `module.init()`, then builds the GraphQL and Fastify instances. Idempotent.
3. `await service.start()` — binds the port and installs signal handlers for graceful shutdown
`createDomainService(options)` is the common one-liner that does steps 1 and 2 together. This is the real quotes service:
```typescript
import {
createDomainService,
type DomainService,
} from "@evolve-framework/core";
import { useClientContext } from "@evolve-framework/core/service/graphql";
import { initClientFactory } from "#src/commercetools.ts";
import { config, loadConfig } from "#src/config.ts";
import { createContext } from "./context.ts";
import { createModule } from "./module.ts";
export const createApp = async (): Promise => {
await loadConfig();
await initClientFactory();
return createDomainService({
name: config.COMPONENT_NAME,
module: createModule(),
graphql: {
context: createContext,
plugins: [useClientContext()],
},
http: {
address: {
host: config.HTTP_HOST,
port: config.HTTP_PORT,
},
},
});
};
export const startServer = async (): Promise => {
const service = await createApp();
await service.start();
};
```
The options:
* **`name`**: service name, used for identification and logging
* **`module`**: the (composite) module providing schema, resolvers, and routes
* **`graphql.context`**: builds the per-request resolver context; required whenever the module provides a GraphQL config
* **`graphql.plugins`**: GraphQL Yoga plugins — `useClientContext()` (from `@evolve-framework/core/service/graphql`) reads the access token from the request and loads the client context
* **`http.address`**: host and port to bind
* **`http.plugins`**: server-wide Fastify plugins
* **`http.bodyLimit`**: max request body size, for services accepting large payloads
* **`http.factory`**: escape hatch that replaces the default Fastify setup entirely — used only by highly custom services such as the GraphQL gateway
After `init()`, tests can use `service.fastify.inject(...)` to make requests without binding a port.
## Anatomy of a service
[Section titled “Anatomy of a service”](#anatomy-of-a-service)
With the framework owning resolvers and schemas, a migrated service is a thin composition layer. The quotes service, for example:
| File | Purpose |
| ------------ | -------------------------------------------------------------------------- |
| `config.ts` | Load and validate environment configuration |
| `module.ts` | `createModule()`: compose framework modules into a `CompositeModule` |
| `context.ts` | Build the per-request GraphQL context (auth, dataloaders, store context) |
| `server.ts` | `createApp()`: wire config, module, and context into `createDomainService` |
| `index.ts` | Entry point: `await startServer()` |
The CMS service is even smaller — the framework’s `initContextValue` (from `@evolve-framework/contentful/graphql/context`) serves directly as the context factory, so the service is little more than configuration plus a `CompositeModule` around `ContentfulModule`.
### Context
[Section titled “Context”](#context)
The `graphql.context` function receives the server context for each request and returns the value resolvers see. For commercetools services this typically combines the store context (read from the `X-StoreContext-*` headers set by the gateway) with a client context that determines how the service authenticates against commercetools:
```typescript
import {
ClientContext,
RemoteClientContextLoader,
StoreContext,
} from "@evolve-framework/commercetools";
import { readStoreContextFromRequest } from "@evolve-framework/core/service/graphql";
```
Most services use `RemoteClientContextLoader`, which delegates token handling to the account service; the account service itself manages tokens directly. `initContextValue` from `@evolve-framework/commercetools` registers the context so the framework’s dataloaders can attach to it.
## Shared infrastructure
[Section titled “Shared infrastructure”](#shared-infrastructure)
Beyond the module system, `@evolve-framework/core` provides:
* **Cache** (`@evolve-framework/core/cache`): a Redis-backed cache handler; call `cache.configure(config.REDIS_URL)` at startup and `cache.close()` on shutdown
* **Resolver utilities** (`@evolve-framework/core/service/graphql`): `assertAuthenticated()`, `validateZodSchema()`, error helpers
* **Observability** (`@evolve-framework/core/observability`): structured logging and OpenTelemetry wiring
* **Messaging and storage** (`@evolve-framework/core/messaging`, `@evolve-framework/core/storage`): publisher and storage abstractions, with concrete bindings in the `cloud-adapter-*` packages
`@evolve-framework/commercetools` adds its own infrastructure: batching dataloaders via `createDataLoaders()`, client context loaders, mappers, Algolia and commercetools-native search, test utilities (`@evolve-framework/commercetools/testing`), and fishery-based test data factories (`@evolve-framework/commercetools/factories`).
## Request flow
[Section titled “Request flow”](#request-flow)
```
sequenceDiagram
participant Client
participant Gateway as GraphQL Gateway
participant Service as Domain Service
participant Module as Module
participant Backend as SAAS Backend
Client->>Gateway: GraphQL query
Gateway->>Service: Federated subquery
Service->>Service: Build context (graphql.context)
Service->>Module: Execute resolver
Module->>Module: DataLoader batching
Module->>Backend: API call
Backend-->>Module: Response
Module->>Module: Mapper transforms data
Module-->>Service: Resolved data
Service-->>Gateway: Subquery result
Gateway-->>Client: Merged response
```
## What stays in your service
[Section titled “What stays in your service”](#what-stays-in-your-service)
Not everything moves into the framework. Your service still owns:
* **Configuration**: environment loading and validation in `config.ts`
* **Composition**: which modules to compose, with which options
* **Context wiring**: your auth pattern in `context.ts`
* **REST endpoints**: project-specific routes beyond what modules register
* **Event handlers and jobs**: service-specific event processing and scheduled work
* **Project-specific modules**: any resolvers, schema extensions, or mappers you add — see [Customization and extensibility](/architecture/core/framework/customization/)
# Composition & deployment topologies
> Why the framework is built around composable modules: choose your own service topology, from a single backend service to one service per domain.
The central idea behind the Evolve framework is that **functionality and deployment are decoupled**. Features ship as *modules* — self-contained bundles of GraphQL schema, resolvers, and HTTP routes published in the `@evolve-framework/*` packages. How many *services* you run is a separate decision, made in your implementation, per environment, and revisable at any time.
This is what makes Evolve work for a two-person team and a twenty-person organization alike: both compose the same modules, they just deploy them differently.
## The module is the unit, the service is a choice
[Section titled “The module is the unit, the service is a choice”](#the-module-is-the-unit-the-service-is-a-choice)
A module (`CatalogModule`, `CheckoutModule`, `ContentfulModule`, `StripePaymentModule`, …) carries everything one capability needs. A service is nothing more than a thin host that mounts one or more modules through `createDomainService`. That gives you a spectrum:
**One backend service.** Compose every module you use into a single `CompositeModule` and run one deployable. `CompositeModule` merges the modules’ schemas and resolvers into one subgraph, concatenates their HTTP routes, and chains their lifecycles. Cross-module references keep working: when one module hands another an entity stub (a CMS page referencing a `ProductVariant` by SKU, say), the composite hydrates it through the owning module’s reference resolver — the same mechanic a federation gateway applies between subgraphs, applied inside a single service.
**One service per domain.** The platform’s default layout: catalog, checkout, account, orders, quotes, and CMS each run as their own service and join the supergraph as separate subgraphs. Each can scale, deploy, and fail independently — and each can be owned by a different team.
**Anything in between.** Because both composition mechanisms produce valid federation subgraphs, you can start with one service and peel domains off as your team or traffic grows — or fold services back together to cut infrastructure cost. The storefront never notices: it talks to one federated graph either way.
A single service hosting three domains
```typescript
export const createModule = (): CompositeModule => {
return new CompositeModule([
new CatalogModule({ clientFactory }),
new CheckoutModule({ config }),
new ContentfulModule({ config: contentfulConfig }),
]);
};
```
Moving `ContentfulModule` out of this composite and into its own service is a topology change, not a code change: the module is instantiated the same way in either host.
## GraphQL federation as the enabler
[Section titled “GraphQL federation as the enabler”](#graphql-federation-as-the-enabler)
Federation is what makes the topology invisible to consumers. The storefront queries one schema through the gateway; whether a field is resolved by a subgraph running alone or by a module inside a composite is a routing detail. Entity references (`@key` types like `Product`, `Customer`, `BusinessUnit`) resolve across whatever boundaries happen to exist — between subgraphs via the gateway’s query planner, and between modules inside one service via the composite’s entity hydration.
This also means the schema contract is stable while the deployment evolves: splitting a service does not break clients, because the supergraph they see is recomposed, not redesigned.
## Where the SaaS integrations sit
[Section titled “Where the SaaS integrations sit”](#where-the-saas-integrations-sit)
Vendor integrations are deliberately packaged *below* the topology decision. `@evolve-framework/commercetools` owns the commerce domain modules, `@evolve-framework/contentful` and `@evolve-framework/storyblok` own CMS modules, and the `commercetools-*` payment packages own PSP integrations. Your services import and configure them; they never contain vendor logic themselves.
The practical consequences:
* **Swapping a vendor is swapping a module.** The platform ships two interchangeable CMS slots (Contentful or Storyblok) — the service code around them is identical.
* **Adding a vendor doesn’t touch the topology.** A new PSP is a new module mounted in an existing (or new) service — see [Adding a payment provider](/how-to/adding-a-payment-provider/).
* **Project-specific code stays in your project.** Custom modules compose alongside framework modules in the same `CompositeModule` — see [Customization](/architecture/core/framework/customization/).
## Local development: the monolith
[Section titled “Local development: the monolith”](#local-development-the-monolith)
The platform’s dev setup exercises this flexibility daily: `pnpm dev` boots every service module inside a single process (the *monolith* runner, one worker thread per module) while production runs them as separate containers. Same modules, different topology — which is exactly the point.
## Choosing a topology
[Section titled “Choosing a topology”](#choosing-a-topology)
Some rules of thumb:
| Situation | Lean toward |
| -------------------------------------------- | --------------------------------------------- |
| Small team, early project | Fewer services — less operational surface |
| Independent team ownership per domain | Service per domain |
| One domain dominates load (e.g. catalog) | Split that domain out, keep the rest together |
| Strict blast-radius or compliance boundaries | Separate services at the boundary |
| Local development | The monolith runner, always |
Whatever you choose, the schema your consumers see — and the module code you maintain — stays the same.
# Customization and extensibility
> How to override resolvers, extend schemas, add project-specific modules, and register custom CMS content-type mappers.
The framework provides sensible defaults for every commerce domain. When your project needs different behavior, you can override individual resolvers, extend schemas with your own modules, or register custom mappers for CMS content types.
## Use modules as-is
[Section titled “Use modules as-is”](#use-modules-as-is)
The simplest approach: compose framework modules without modification. This is the right choice when the default resolvers and schemas match your requirements.
```typescript
import { QuoteModule } from "@evolve-framework/commercetools";
import { CompositeModule } from "@evolve-framework/core";
export const createModule = (): CompositeModule => {
return new CompositeModule([new QuoteModule()]);
};
```
## Override individual resolvers
[Section titled “Override individual resolvers”](#override-individual-resolvers)
When you need to change how a specific operation works, subclass the framework module and replace the resolver entry:
```typescript
import { QuoteModule } from "@evolve-framework/commercetools";
import { customQuoteRequestCreate } from "./resolvers/quote-request-create.ts";
export class ProjectQuoteModule extends QuoteModule {
constructor() {
super();
this.resolvers = {
...this.resolvers,
Mutation: {
...this.resolvers.Mutation,
quoteRequestCreate: customQuoteRequestCreate,
},
};
}
}
// Compose with your override instead of the default:
const module = new CompositeModule([new ProjectQuoteModule()]);
```
This replaces only the `quoteRequestCreate` mutation. All other quote resolvers keep their default behavior, and the schema is unchanged.
Alternatively, compose a small module *after* the framework module: `CompositeModule` merges resolvers per GraphQL type name, and the later module in the array wins for each field. Note that a module only contributes when it has type definitions — an override module must carry `typedefs` for the fields it re-resolves, and those definitions must match the framework’s exactly or the merge throws at startup. For plain resolver swaps, subclassing is usually the cleaner option.
## Add project-specific modules
[Section titled “Add project-specific modules”](#add-project-specific-modules)
For functionality that does not exist in the framework, create your own module by extending `AbstractModule`:
```typescript
import { AbstractModule } from "@evolve-framework/core";
import { gql } from "graphql-tag";
import { loyaltyResolver } from "./resolvers/loyalty.ts";
export class LoyaltyModule extends AbstractModule {
typedefs = gql`
type LoyaltyAccount {
points: Int!
tier: String!
}
extend type Customer {
loyalty: LoyaltyAccount
}
`;
resolvers = {
Customer: {
loyalty: loyaltyResolver,
},
};
}
```
Compose it alongside framework modules:
```typescript
const module = new CompositeModule([
new CustomerModule({ config: customerConfig }),
new LoyaltyModule(),
]);
```
`CompositeModule` merges all type definitions and resolvers, so your `extend type Customer` works seamlessly with the framework’s customer schema. If your module needs an SDK client or other side-effecty setup, put it in `init()` — constructors must stay cheap so codegen tooling can instantiate the module without runtime config.
A custom module can also expose HTTP routes by implementing `getHttpConfig()`; the composite mounts them on the service’s Fastify instance alongside the module-provided routes such as CMS webhooks.
## Module configuration
[Section titled “Module configuration”](#module-configuration)
Modules take typed options through their constructor — each declares exactly what it needs, from a `clientFactory` for most commercetools modules to a full config object:
```typescript
new CustomerModule({
config: {
CTP_PROJECT_KEY: config.CTP_PROJECT_KEY,
CTP_AUTH_URL: config.CTP_AUTH_URL,
CTP_USER_CLIENT_ID: config.CTP_USER_CLIENT_ID,
CTP_USER_CLIENT_SECRET: config.CTP_USER_CLIENT_SECRET,
},
});
```
Because the options are constructor arguments, misconfiguration is a type error at the composition site rather than a runtime failure in a resolver.
## Custom CMS content types: the mapper registry
[Section titled “Custom CMS content types: the mapper registry”](#custom-cms-content-types-the-mapper-registry)
The CMS packages map CMS entries onto the shared content schema (blocks, column entries, links). The mapping is driven by a `MapperRegistry` keyed by content type id, and this registry is the primary customization point for CMS services: you rarely touch resolvers, you register mappers.
`ContentfulModule` builds the registry during `init()`: it installs the default mappers for the built-in content types, then invokes your optional `mappers` callback. Registering a mapper for an already-known content type overrides the default without throwing.
```typescript
import type { ColumnEntryMapper } from "@evolve-framework/contentful";
import { ContentfulModule } from "@evolve-framework/contentful";
import { NoopPublisher } from "@evolve-framework/core/messaging";
import type { Entry } from "contentful";
import type { TypePageTitleSkeleton } from "#src/cftypes/index.ts";
const mapPageTitle: ColumnEntryMapper = (context, entry) => {
const data = entry as Entry<
TypePageTitleSkeleton,
"WITHOUT_UNRESOLVABLE_LINKS",
string
>;
return {
__typename: "PageTitle",
id: data.sys.id,
title: data.fields.title.toUpperCase(),
};
};
new ContentfulModule({
config: {
spaceId: config.CONTENTFUL_SPACE_ID,
environment: config.CONTENTFUL_ENVIRONMENT,
accessToken: config.CONTENTFUL_ACCESS_TOKEN,
previewToken: config.CONTENTFUL_PREVIEW_TOKEN,
managementToken: config.CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN,
},
messagePublisher: new NoopPublisher(),
mappers: (registry) => {
registry.registerColumnEntry("pageTitle", mapPageTitle);
},
});
```
The registry has three registration methods, one per mapper kind:
* **`registerBlock(contentTypeId, mapper)`**: top-level page blocks (defaults include `hero`, `productRow`, `uspList`, `contentTeaserRow`)
* **`registerColumnEntry(contentTypeId, mapper)`**: entries inside column blocks (defaults include `richText`, `faq`, `image`, `pageTitle`)
* **`registerLink(contentTypeId, mapper)`**: link-like entries (defaults include `pageContent`, `pageCatalog`, `resourceLink`)
Entries with no registered mapper are logged and skipped, so an unknown content type degrades to a missing block rather than an error.
For an entirely new content type, register a mapper *and* extend the content schema: compose a small `AbstractModule` alongside `ContentfulModule` that adds your GraphQL type (for example a new `Block` union member), then return that `__typename` from your mapper.
Upcoming
An authoring module for Contentful (`ContentfulAuthoringModule`, composed alongside `ContentfulModule` in the same subgraph) is in development and not part of the released packages yet.
## Testing approach
[Section titled “Testing approach”](#testing-approach)
The framework ships with its own test suite covering default behavior. Your project tests should focus on what you own:
* **Custom resolvers and mappers**: test any resolver or mapper you override or add
* **Module composition**: verify that your module combination produces the expected schema (composition errors such as type conflicts surface at `init()`)
* **REST endpoints, event handlers, and jobs**: test project-specific business logic
### Test utilities
[Section titled “Test utilities”](#test-utilities)
`DomainService` itself is test-friendly: after `init()` (or `createDomainService()`), use `service.fastify.inject(...)` to make requests without binding a port.
The `@evolve-framework/commercetools/testing` export provides `createTestServer()` and `createTestExecutor()` for GraphQL integration tests, plus `ctMock` and `ctMockClient` for mocking commercetools API calls. The `@evolve-framework/commercetools/factories` export provides fishery-based factories for generating realistic test data (cart drafts, customer drafts, categories, and more) instead of writing mock data by hand.
Note
If you override a framework resolver or mapper, you take ownership of testing it. The framework tests only cover default behavior.
# Migration guide
> Step-by-step guide for migrating an implementation from the vendored in-repo framework packages to registry-published versions.
The framework packages are no longer distributed as source code in a `vendor/packages/` directory. They are published as `@evolve-framework/*` packages to the Evolve registry and consumed like any other dependency. This guide walks through migrating an implementation from the vendored, `workspace:*`-based setup to registry consumption, and through the module API changes that landed along the way.
## Before you start
[Section titled “Before you start”](#before-you-start)
You need read access to the Evolve registry (`npm.registry.evolve-platform.com`). Locally this means an `NPM_TOKEN`; in CI, authentication happens via OIDC (`evolve-platform/registry-login`).
## Step 1: configure the registry scope
[Section titled “Step 1: configure the registry scope”](#step-1-configure-the-registry-scope)
Point the `@evolve-framework` scope at the Evolve registry in your project’s `.npmrc`:
```ini
@evolve-framework:registry=https://npm.registry.evolve-platform.com
//npm.registry.evolve-platform.com/:_authToken=${NPM_TOKEN}
```
Every other scope keeps resolving from the public npm registry.
## Step 2: pin versions in the catalog
[Section titled “Step 2: pin versions in the catalog”](#step-2-pin-versions-in-the-catalog)
Framework versions are pinned once, in the `catalog:` section of `pnpm-workspace.yaml`. Individual `package.json` files reference the catalog instead of a version or a workspace link:
pnpm-workspace.yaml
```yaml
catalog:
"@evolve-framework/core": 0.1.0
"@evolve-framework/schemas": 0.1.0
"@evolve-framework/commercetools": 0.1.0
```
backend/services/account-commercetools/package.json
```diff
"dependencies": {
"@evolve-framework/core": "workspace:*",
"@evolve-framework/commercetools": "workspace:*",
"@evolve-framework/core": "catalog:",
"@evolve-framework/commercetools": "catalog:",
}
```
Upgrading the framework later is a one-line change: bump the version in the catalog and run `pnpm install`. Every package that references `catalog:` picks it up automatically.
## Step 3: remove the vendored packages
[Section titled “Step 3: remove the vendored packages”](#step-3-remove-the-vendored-packages)
Drop the vendor directory from your workspace configuration and delete it:
pnpm-workspace.yaml
```diff
packages:
- "frontend/*"
- "packages/*"
- "vendor/packages/*"
```
Run `pnpm install` afterwards. The lockfile should now resolve `@evolve-framework/*` from the registry instead of linking local directories, and your build no longer needs to compile the framework before your services.
## Step 4: adopt the module-based service anatomy
[Section titled “Step 4: adopt the module-based service anatomy”](#step-4-adopt-the-module-based-service-anatomy)
The vendored era’s GraphQL-centric API — `AbstractGraphQLModule`, `GraphQLCompositeModule`, and passing `typeDefs`/`resolvers` into `new DomainService()` — was replaced by a general module system: `Module` / `AbstractModule` / `CompositeModule`. A module carries its own schema, resolvers, *and* HTTP routes; the service just composes modules and no longer manually wires GraphQL artifacts or process lifecycle.
Before:
src/server.ts (old)
```typescript
import { DomainService, ProcessManager } from "@evolve-framework/core";
import {
CustomerGraphQLModule,
GraphQLCompositeModule,
} from "@evolve-framework/commercetools";
const module = new GraphQLCompositeModule([new CustomerGraphQLModule()]);
const app = new DomainService({
name: config.COMPONENT_NAME,
graphql: {
typeDefs: module.getTypedefs(),
resolvers: module.getResolvers(),
context: newContext,
},
http: {
address: { host: config.HTTP_HOST, port: config.HTTP_PORT },
},
});
const pm = new ProcessManager({
start: () => app.start(),
stop: () => app.stop(),
});
await pm.start();
```
After:
src/server.ts (new)
```typescript
import { CompositeModule, createDomainService } from "@evolve-framework/core";
import {
AuthModule,
CustomerModule,
} from "@evolve-framework/commercetools";
const service = await createDomainService({
name: config.COMPONENT_NAME,
module: new CompositeModule([
new AuthModule({ config, clientFactory }),
new CustomerModule({ config }),
]),
graphql: {
context: createContext,
},
http: {
address: { host: config.HTTP_HOST, port: config.HTTP_PORT },
},
});
await service.start();
```
The key differences:
* **One `module` option** replaces `graphql.typeDefs` and `graphql.resolvers`. The service extracts schema, resolvers, HTTP routes, and Fastify plugins from the module tree itself.
* **`createDomainService`** is a convenience factory that constructs the service and awaits its `init()`. `graphql.context` and `graphql.plugins` still work as before.
* **`ProcessManager` is no longer public API.** `service.start()` handles SIGTERM/SIGINT, graceful shutdown, and module cleanup internally.
* Project-specific code (custom REST endpoints, event handlers) becomes a module too: implement `Module` or extend `AbstractModule` and add it to the `CompositeModule`. See [module composition](/architecture/core/framework/architecture/#the-module-system) and [module configuration](/architecture/core/framework/architecture/#module-configuration).
## A migration plan for teams
[Section titled “A migration plan for teams”](#a-migration-plan-for-teams)
Treat the migration as a sequence of small, verifiable steps rather than one big change. A phasing that has worked well:
1. **Inventory.** Find everything that touches the old world: imports of `@evolve-packages/*`, `vendor/packages/` paths, and uses of the retired API (see the mapping table below). This list is your work queue and your progress tracker.
2. **Mechanical swap first.** Registry scope, `catalog:` pins, and the import renames are low-risk and cover most files. Land them as one reviewable change with `pnpm check` green.
3. **Then one service at a time.** Adopt the module anatomy per service — each is a small, independently testable diff. Start with your simplest service to establish the pattern; save services with custom REST routes or event handlers for last.
4. **Verify each step the same way**: `pnpm install`, `pnpm codegen`, `pnpm check`, `pnpm test`, then boot (`pnpm dev`) and hit the service’s `/healthcheck` and a representative GraphQL query.
### API mapping
[Section titled “API mapping”](#api-mapping)
| Old (vendored era) | New |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `AbstractGraphQLModule` | `AbstractModule` (`@evolve-framework/core`) |
| `GraphQLCompositeModule` | `CompositeModule` (`@evolve-framework/core`) |
| `module.getTypedefs()` / `module.getResolvers()` | `module.getGraphQLConfig()` (consumed by the service internally) |
| `new DomainService({ graphql: { typeDefs, resolvers } })` | `createDomainService({ name, module, graphql: { context }, http })` |
| `ProcessManager` | gone — `service.start()` handles lifecycle |
| `init.ts` + `context.ts` service anatomy | `config.ts` + `module.ts` + `server.ts` + `index.ts` |
| `@evolve-packages/observability` | `@evolve-framework/core/observability` |
| `@evolve-packages/cache` | `@evolve-framework/core/cache` |
| `@evolve-packages/messaging` | `@evolve-framework/core/messaging` + `@evolve-framework/cloud-adapter-{aws,azure,gcp}` |
| `@evolve-packages/mcp-core` | `@evolve-framework/mcp-core` |
| `vendor/packages/commercetools/src/factories` | `@evolve-framework/commercetools/factories` |
## Using AI coding agents
[Section titled “Using AI coding agents”](#using-ai-coding-agents)
The migration is well-suited to AI coding agents: the transformation is pattern-based, the verification is scriptable, and this page plus the [architecture](/architecture/core/framework/architecture/) page give an agent the ground truth it needs. Some example prompts, written to be pasted as-is (adjust paths to your project):
**Inventory:**
> Scan this repository for consumption of the old vendored Evolve framework. Produce a table of: (1) every import from `@evolve-packages/*` or a `vendor/packages/` path, (2) every use of `AbstractGraphQLModule`, `GraphQLCompositeModule`, `new DomainService(`, or `ProcessManager`, (3) every service still using the `init.ts`/`context.ts` anatomy. Group by package/service and estimate which services are simplest to migrate first. Do not change anything yet.
**Migrate one service:**
> Migrate `backend/services/` to the current Evolve service anatomy: `src/config.ts` (enviconf class), `src/module.ts` exporting `createModule()` returning a `CompositeModule`, `src/server.ts` using `createDomainService({ name, module, graphql: { context }, http })`, and a thin `src/index.ts` exporting and calling `startServer`. Use `backend/services/quotes-commercetools` as the reference. Replace retired imports per the mapping table in docs/architecture/core/framework/migration-guide.md. Preserve all existing behavior, including custom REST routes (move them into a module’s `getHttpConfig()`). Then run `pnpm check` and the service’s tests, and fix what fails. Show me the diff before committing.
**Verify:**
> The migration of `` is complete. Verify it end to end: boot the service, confirm `/healthcheck` responds, run one representative GraphQL query against it with the store-context headers set, and compare the service’s `schema.generated.graphql` before and after the migration — the schema must be unchanged. Report findings; do not fix anything without showing me first.
Two practical tips when delegating this: give the agent one service per task (reviewable diffs beat big-bang branches), and insist on the schema-diff check — an unchanged subgraph schema is the strongest signal that a migration preserved behavior.
## Consuming snapshot releases
[Section titled “Consuming snapshot releases”](#consuming-snapshot-releases)
Alongside stable releases, the registry receives **snapshot versions** of all packages on a continuous basis. Snapshots are versioned `0.0.0--` and published under the `snapshot` dist-tag.
To try an unreleased fix, pin the exact snapshot version in your catalog:
pnpm-workspace.yaml
```diff
catalog:
"@evolve-framework/commercetools": 0.1.0
"@evolve-framework/commercetools": 0.0.0-7ca0d25c3-20260703132118
```
Snapshots are immutable and safe to pin temporarily, but move back to a stable version once the change is released.
## GraphQL codegen
[Section titled “GraphQL codegen”](#graphql-codegen)
Nothing changes here compared to the vendored setup. Each service keeps its own `codegen.ts` and `.graphql` schema files; the framework’s types ship pre-generated inside the packages. Your codegen only needs to cover project-specific schema extensions.
## Gateway
[Section titled “Gateway”](#gateway)
The gateway is not affected by this migration. It is a standalone Rust Hive Router binary and does not consume the `@evolve-framework/*` npm packages — see the [roadmap](/architecture/core/framework/roadmap/#gateway-extraction) for how it shipped.
# Roadmap
> What the framework has delivered — extraction, registry publishing, implementation packages, gateway — and what comes next.
The framework started as an in-repo extraction and has since become a standalone, versioned product. This page tracks what has shipped and what is still ahead. No dates — items land when they are ready.
## Delivered
[Section titled “Delivered”](#delivered)
### Framework extraction
[Section titled “Framework extraction”](#framework-extraction)
The framework lives in its own repository (`evolve-framework`) instead of a `vendor/packages/` directory inside each implementation. Implementations consume it as a regular dependency; the [migration guide](/architecture/core/framework/migration-guide/) covers moving over.
### Registry publishing
[Section titled “Registry publishing”](#registry-publishing)
Packages are published as `@evolve-framework/*` to the Evolve registry (`npm.registry.evolve-platform.com`). Releases are driven by changesets: merged changesets accumulate into a release PR on `main`, and every push that does not cut a stable release publishes snapshot versions (`0.0.0--`, dist-tag `snapshot`) for early testing.
### Implementation packages
[Section titled “Implementation packages”](#implementation-packages)
The package family the original roadmap promised largely exists:
* **Commerce**: `@evolve-framework/commercetools` and `@evolve-framework/sfcc` (Salesforce Commerce Cloud)
* **CMS**: `@evolve-framework/contentful` and `@evolve-framework/storyblok`
* **Payments**: Adyen, Stripe, Buckaroo, Pay.nl, Invoice, and Mollie — each as its own `@evolve-framework/commercetools-*` package
* **Cloud adapters**: `@evolve-framework/cloud-adapter-aws`, `-azure`, and `-gcp` abstract messaging and storage per cloud
All build on the same `@evolve-framework/core` and `@evolve-framework/schemas` layers.
### Gateway extraction
[Section titled “Gateway extraction”](#gateway-extraction)
The gateway is extracted — but not as the `@evolve-framework/gateway` npm package once planned here. The Node/Apollo gateway was replaced by a custom **Rust Hive Router binary** (`backend/services/graphql-gateway`):
* The `@labdigital/federated-token` gateway behaviour is reimplemented as a compiled-in Rust plugin (`federated_token`, maintained in its own repo, `evolve-platform/hive-router-federated-token`) with a byte-for-byte compatible wire format, so subgraphs are unaffected.
* Trusted documents come from Hive Router’s native `persisted_documents` support against the Hive CDN, replacing the hand-rolled plugin.
A project’s gateway is now the router binary plus a `router.config.yaml` and a supergraph — configuration rather than code, which was the point.
## Upcoming
[Section titled “Upcoming”](#upcoming)
### Evolve CLI
[Section titled “Evolve CLI”](#evolve-cli)
Scaffolding and maintenance tooling: `create-evolve-app` to generate a project from choices (commerce backend, CMS, payment providers, deployment target), `evolve upgrade` for version bumps and codemods, and `evolve add` for dropping modules into an existing composition. Not started — `@evolve-framework/packager` exists but is a build/deployment packager, not this CLI.
### Frontend framework package
[Section titled “Frontend framework package”](#frontend-framework-package)
The extraction pattern applied to the storefront: shared UI components, hooks, and utilities as framework packages so a project’s frontend carries only project-specific code.
### Authoring stack
[Section titled “Authoring stack”](#authoring-stack)
An AI-assisted content authoring layer, currently on feature branches and not yet released: an authoring console and in-storefront chrome with review/publish workflows, a `ContentfulAuthoringModule` (`@evolve-framework/contentful/authoring`) exposing the authoring queries, mutations, and OAuth routes, and a gateway `authoring_token` plugin that propagates the editor’s CMS token to subgraphs.
# GraphQL Federation
> GraphQL Federation is the API integration technology in Evolve.
Evolve relies heavily on GraphQL features, particularly Federation, to provide a flexible backend API. It allows us to provide a single consistent (public) API to the consuming clients, while in the back-end we can be flexible and integrate any composition of services we want.
## Implementation pattern
[Section titled “Implementation pattern”](#implementation-pattern)
Typically, a federated architecture consists of a GraphQL gateway that loads a `supergraph` that is composed of several `subgraphs`, which in turn implement `resolvers` to integrate with underlying SAAS services, custom services or sit in front of existing on-prem REST services.
The GraphQL gateway is the accesspoint to any client that interacts with your composable architecture.
```
graph TD;
web<-->graphql-gateway
mobile-app<-->graphql-gateway
LLM<-->mcp-server["MCP Server"]
mcp-server<-->graphql-gateway
graphql-gateway<-->supergraph
supergraph<-->subgraph1
supergraph<-->subgraph2
supergraph<-->subgraph3
subgraph1<-->SAAS-service
subgraph2<-->custom-service
subgraph3<-->existing-REST-service
```
Where traditional clients like web and mobile apps interact with the GraphQL gateway directly, AI clients such as LLMs connect through an [MCP server](/architecture/mcp/). The MCP server translates GraphQL operations into tools that AI agents can discover and invoke using the [Model Context Protocol](https://modelcontextprotocol.io/), handling authentication, session management, and rate limiting along the way.
Having a `subgraph` implement the underlying services our platform depends on, allows us to define our own data models instead of relying on the data model provided by the external service. This gives us more control and less (vendor) dependency on the underlying services. This in turn makes it easier for our implementation to Evolve and potentially replace individual subgraphs over time.
## Technology stack
[Section titled “Technology stack”](#technology-stack)
Evolve implements [Apollo Federation v2](https://www.apollographql.com/docs/graphos/schema-design/federated-schemas/federation), an open specification for composing multiple GraphQL services into a single supergraph. Because the spec is open, each layer of the stack can use the best tool for the job, and individual components can be swapped independently.
**Gateway**: a custom [Hive Router](https://the-guild.dev/graphql/hive/docs/router) binary written in Rust (`backend/services/graphql-gateway`), with the Evolve plugins compiled in. It runs the supergraph and handles query planning, routing requests to the right subgraphs, and merging responses. In development the supergraph is composed locally with the Hive CLI (`task supergraph` → `supergraph.generated.graphql`); in production the router polls the supergraph from the Hive CDN. The router also enforces persisted documents in production (`require_id: true`, backed by Hive App Deployments), rejecting any operation not registered in the Hive CDN.
**Subgraphs**: each domain service runs [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) as its GraphQL server. Yoga is lightweight and well-suited for subgraph services that don’t need the full Apollo Server feature set. Subgraph schemas are annotated with Apollo Federation directives (`@key`, `@extends`, `@external`, and others) using `@apollo/subgraph`.
**Schema management**: [Hive](https://the-guild.dev/graphql/hive) serves as the schema registry and supergraph delivery layer. Subgraphs publish their schemas to Hive, which composes the supergraph and delivers it to the gateway via CDN. Hive also provides usage analytics and schema change validation.
# Page model
> How Evolve structures pages across CMS and catalog services using GraphQL Federation.
Evolve uses two complementary page systems: **CMS-managed pages** for content-driven experiences and **catalog pages** for category browsing driven by the commerce backend.
## CMS-managed pages
[Section titled “CMS-managed pages”](#cms-managed-pages)
CMS-managed pages are created and maintained in the CMS (Contentful or Storyblok). They share a common interface and are returned through a `Page` union type:
```graphql
interface AbstractPage {
id: ID!
name: String!
path: String!
translatedPaths: [TranslatedPath!]
breadcrumbs: [Breadcrumb!]
meta: PageMeta
}
union Page = ContentPage | CatalogPage
```
### ContentPage
[Section titled “ContentPage”](#contentpage)
A free-form content page composed of blocks managed in the CMS. Use this for pages like “About us”, landing pages, or editorial content.
```graphql
type ContentPage implements AbstractPage @key(fields: "id") {
id: ID!
name: String!
path: String!
translatedPaths: [TranslatedPath!]
breadcrumbs: [Breadcrumb!]
meta: PageMeta
hero: HeroBlock
body: [Block]
}
```
Query a content page by path:
```graphql
query GetContentPage($path: String!) {
contentPage(path: $path) {
name
meta { title, description }
breadcrumbs { name, path, type }
hero { ... }
body { ... }
}
}
```
### CatalogPage
[Section titled “CatalogPage”](#catalogpage)
A product listing page where the content and product selection are defined in the CMS. The CMS service owns the page content (hero, top/bottom content sections, teasers), while the catalog service extends it with product listing configuration through GraphQL Federation.
```graphql
# CMS service owns the content fields
type CatalogPage implements AbstractPage @key(fields: "id") {
id: ID!
name: String!
path: String!
translatedPaths: [TranslatedPath!]
breadcrumbs: [Breadcrumb!]
meta: PageMeta
hero: HeroBlock
topContent: [Block!]
bottomContent: [Block!]
categoryId: String
teaserRow: TeasersBlock
}
# Catalog service extends with product listing config
type CatalogPage @extends @key(fields: "id") {
id: ID!
categoryId: String @external
productListingConfig: ProductListingConfig!
@requires(fields: "categoryId")
}
```
This is a good example of how GraphQL Federation works in Evolve: the CMS service resolves the content fields, the catalog service resolves `productListingConfig` by reading the `categoryId` that the CMS provides, and the gateway merges both into a single response.
Query a catalog page by path:
```graphql
query GetCatalogPage($path: String!) {
catalogPage(path: $path) {
name
categoryId
meta { title, description }
breadcrumbs { name, path, type }
productListingConfig {
categoryId
defaultOrder
prefilters { key, value }
}
topContent { ... }
bottomContent { ... }
}
}
```
### Generic page query
[Section titled “Generic page query”](#generic-page-query)
When you don’t know the page type ahead of time, use the `page` query which returns the `Page` union:
```graphql
query GetPage($path: String!) {
page(path: $path) {
... on ContentPage {
name
body { ... }
}
... on CatalogPage {
name
productListingConfig { categoryId }
}
}
}
```
All three CMS page queries (`contentPage`, `catalogPage`, `page`) accept optional `version` and `previewID` arguments for draft/preview content.
## Catalog pages
[Section titled “Catalog pages”](#catalog-pages)
Catalog pages are resolved directly from the commerce backend without CMS involvement. They are standalone types (not part of the `Page` union) and are owned entirely by the catalog service.
### ProductCategoryPage
[Section titled “ProductCategoryPage”](#productcategorypage)
Renders all products in a specific product category. The category hierarchy in the commerce backend is the source of truth.
```graphql
type ProductCategoryPage {
meta: PageMeta
path: String!
name: String
indexable: Boolean
category: ProductCategory!
productListingConfig: ProductListingConfig!
breadcrumbs: [Breadcrumb!]!
}
```
Query by path:
```graphql
query {
productCategoryPageByPath(path: "/shoes/running") {
name
category { name, slug }
productListingConfig {
categoryId
defaultOrder
enabledFacets
}
breadcrumbs { name, path }
}
}
```
Note
Use `CatalogPage` when content editors need control over the page layout (hero images, editorial content blocks, teaser rows). Use `ProductCategoryPage` when the category structure in the commerce backend should drive the page directly.
### CategoryOverviewPage
[Section titled “CategoryOverviewPage”](#categoryoverviewpage)
Displays child categories for a parent category, useful for top-level navigation pages.
```graphql
type CategoryOverviewPage {
meta: PageMeta
path: String!
name: String
indexable: Boolean
category: ProductCategory
childCategories: [ProductCategory!]!
breadcrumbs: [Breadcrumb!]!
}
```
## Product detail pages
[Section titled “Product detail pages”](#product-detail-pages)
Product detail pages don’t have a dedicated page type. The frontend queries the `product` field directly:
```graphql
query GetProductDetailPage($slug: String!) {
product(slug: $slug) {
name
brand
slug
meta { title, description }
breadcrumbs { name, path, type }
variant(sku: $selectedSku) {
sku
availability
description
price {
gross { centAmount, currencyCode }
net { centAmount, currencyCode }
}
images { url, alt }
}
}
}
```
## Search pages
[Section titled “Search pages”](#search-pages)
Search pages also don’t have a dedicated page type. The frontend uses the `productSearch` query from the catalog service:
```graphql
query SearchProducts(
$searchTerm: String
$page: Int!
$pageSize: Int!
$sort: ProductSortOrder!
$filters: [FacetFilterInput!]
) {
productSearch(
searchTerm: $searchTerm
page: $page
pageSize: $pageSize
sort: $sort
filters: $filters
) {
total
items { ... }
facets { ... }
}
}
```
## Shared types
[Section titled “Shared types”](#shared-types)
Several types are shared across page types using the `@shareable` federation directive:
```graphql
type PageMeta @shareable {
title: String
description: String
keywords: [String]
openGraph: OpenGraph
}
type Breadcrumb @shareable {
type: BreadcrumbType!
name: String!
path: String!
}
enum BreadcrumbType {
CATEGORY
CONTENT
PRODUCT
}
```
Both the CMS and catalog services resolve `PageMeta` and `Breadcrumb` independently, with the `@shareable` directive telling the gateway that either service can provide these types.
# Multi-store support
> How Evolve supports multiple stores, locales, and currencies through the StoreContext pattern.
Evolve supports multiple stores, locales, and currencies from a single backend deployment. Every request carries a **StoreContext** that tells each service which store, language, and currency to use. This enables scenarios like running separate country stores, localized storefronts, or B2B stores with different assortments from the same infrastructure.
## StoreContext
[Section titled “StoreContext”](#storecontext)
The StoreContext has three properties:
* **`storeKey`**: identifies the store
* **`locale`**: the language for the request (e.g. `nl-NL`, `en-GB`)
* **`currency`**: the currency for prices and carts (e.g. `EUR`, `GBP`)
These three values flow through the entire request lifecycle, from the frontend through the gateway to every domain service. All services receive the same StoreContext, regardless of which SAAS backend they integrate with.
## How it flows
[Section titled “How it flows”](#how-it-flows)
```
sequenceDiagram
participant Frontend
participant Gateway as GraphQL Gateway
participant Commerce as Commerce Service
participant CMS as CMS Service
Frontend->>Gateway: GraphQL request + headers X-StoreContext-StoreKey X-StoreContext-Locale X-StoreContext-Currency
Gateway->>Gateway: Read headers into StoreContext
Gateway->>Commerce: Federated subquery (headers forwarded)
Gateway->>CMS: Federated subquery (headers forwarded)
Commerce->>Commerce: readStoreContextFromRequest()
CMS->>CMS: readStoreContextFromRequest()
Commerce-->>Gateway: Store-scoped, localized data
CMS-->>Gateway: Localized content
Gateway-->>Frontend: Merged response
```
### Frontend
[Section titled “Frontend”](#frontend)
The frontend sets the three `X-StoreContext-*` headers on every GraphQL request, both server-side and client-side:
```typescript
const fetcher = initClientFetcher("/graphql", {
defaultHeaders: {
"X-StoreContext-StoreKey": storeConfig.storeKey,
"X-StoreContext-Locale": locale,
"X-StoreContext-Currency": storeConfig.currency,
},
});
```
The **store key** and **currency** come from the store configuration (selected per deployment via the `STORE_KEY` environment variable). The **locale** comes from the URL path via next-intl routing (e.g. `/nl-NL/products/...`).
### Gateway
[Section titled “Gateway”](#gateway)
The GraphQL gateway (Hive Router) forwards the headers from the incoming request to every subgraph service. This is configured declaratively in `router.config.yaml`:
```yaml
headers:
all:
request:
- propagate:
named:
- X-StoreContext-StoreKey
- X-StoreContext-Locale
- X-StoreContext-Currency
```
### Domain services
[Section titled “Domain services”](#domain-services)
Each service reads the headers from the incoming request and constructs a `StoreContext` object that resolvers can access via `context.storeContext`:
```typescript
import { readStoreContextFromRequest } from "@evolve-framework/core/service/graphql";
const storeContext = readStoreContextFromRequest(request);
```
All three headers are required. If any is missing, the request fails with a GraphQL error.
## How services use StoreContext
[Section titled “How services use StoreContext”](#how-services-use-storecontext)
Every service uses StoreContext, but what each property means depends on the integration.
### Commerce services
[Section titled “Commerce services”](#commerce-services)
Commerce services use all three properties to scope operations to the correct store:
* **`storeKey`**: scopes API calls to a specific store. Carts, orders, customers, and shopping lists are all queried within the store context.
* **`locale`**: determines which translations are returned for product names, descriptions, and category names.
* **`currency`**: selects the correct prices. Together with the country (derived from the locale), it drives price selection, tax calculation, and cart operations.
* **Product projections**: the store context provides parameters for product queries that filter results by store assortment, locale, and pricing.
### CMS services
[Section titled “CMS services”](#cms-services)
CMS services (Contentful, Storyblok) use StoreContext primarily for content localization:
* **`locale`**: determines which language version of content to fetch from the CMS. Contentful uses it to filter entries by locale, Storyblok extracts the language code for its API.
* **`storeKey`**: used in cache keys to keep content separated per store when different stores serve different content.
* **`currency`**: included in cache keys to ensure different currency contexts don’t share cached data.
### Cache scoping
[Section titled “Cache scoping”](#cache-scoping)
All services use StoreContext to generate cache key prefixes. This ensures that cached data from one store, locale, or currency does not leak into another:
```typescript
public get cacheKeyPrefix(): string {
return `${this.storeKey}-${this.locale}-${this.currency}`;
}
```
## Store configuration
[Section titled “Store configuration”](#store-configuration)
Each frontend deployment is configured for a specific store through the `STORE_KEY` environment variable. This selects a store configuration that defines:
* **Store key**: identifies the store across all backend services
* **Default locale and available locales**: which languages the store supports
* **Currency**: the store’s currency
* **Country**: used for tax and shipping calculations
* **Available payment methods**: which payment providers are enabled
* **Checkout configuration**: checkout-specific settings
* **Distribution channel**: for store-specific product assortment and pricing
To run multiple stores, deploy multiple frontend instances with different `STORE_KEY` values. They all share the same backend.
## B2B store override
[Section titled “B2B store override”](#b2b-store-override)
In B2B scenarios, a customer may belong to a business unit that is assigned to a specific store. When this is the case, the `ClientContext` overrides the store key from the request with the business unit’s store key:
```typescript
get storeKey(): string {
if (!this.isAuthenticated()) {
return this.storeContext.storeKey;
}
return this._info?.businessContext?.storeKey
?? this.storeContext.storeKey;
}
```
This allows B2B customers to see store-specific assortments and pricing based on their business unit, even when accessing the platform through a shared storefront. See [B2B & B2C architecture](/architecture/b2b/) for more details on business units and the BusinessContext.
# Frontend
> The Next.js storefront: rendering strategy, routing, CMS abstraction, and image optimization.
The Evolve storefront is a Next.js 16 application using the App Router, React 19, Tailwind CSS 4, and TypeScript. It is designed for fast anonymous page loads through aggressive CDN caching while providing full server-side rendering for authenticated sessions.
## Rendering strategy
[Section titled “Rendering strategy”](#rendering-strategy)
| Visitor type | Behaviour |
| ------------- | --------------------------------------------------------------------------------------- |
| Anonymous | CDN cache: `s-maxage=900; stale-while-revalidate=2592000` (15 min fresh, 30 days stale) |
| Authenticated | SSR on every request (no CDN cache) |
The `next.config.ts` conditionally sets the `Cache-Control` header based on the absence of `userToken` and `userData` cookies, so returning customers always see fresh data.
## Internationalization
[Section titled “Internationalization”](#internationalization)
Routing is handled by `next-intl` with the `always` prefix strategy, so every URL starts with a locale segment. Eight locales are supported:
| Locale | URL prefix |
| ------- | ---------- |
| `nl-NL` | `/nl` |
| `en-GB` | `/en` |
| `de-DE` | `/de` |
| `fr-FR` | `/fr` |
| `es-ES` | `/es` |
| `it-IT` | `/it` |
| `nl-BE` | `/be/nl` |
| `fr-BE` | `/be/fr` |
Pathnames are localized per language (e.g. `/nl/winkelwagen`, `/de/warenkorb`, `/en/cart`). The mapping is defined in `packages/site-config/src/i18n/config.ts`.
## Rewrites
[Section titled “Rewrites”](#rewrites)
The root `/:locale` path and catch-all fallback both rewrite to `/content/*` routes, letting the CMS handle homepage and content pages without a separate routing layer. This is configured in `next-rewrites.ts`.
## CMS abstraction
[Section titled “CMS abstraction”](#cms-abstraction)
The `EVOLVE_CMS` environment variable (`"contentful"` or `"storyblok"`) controls which preview provider and data-attribute helper is active. This allows the same codebase to work with either CMS without build-time branching. Preview API routes are mounted at `/api/preview` (Storyblok) and `/api/preview-contentful` (Contentful).
## Image optimization
[Section titled “Image optimization”](#image-optimization)
A custom image loader (`src/lib/image-loader.ts`) detects the CDN origin from the image URL and applies the correct transform syntax:
| Source | Transform example |
| --------------- | ---------------------------------- |
| Storyblok | `/m/{width}x0/filters:quality(80)` |
| Amplience | `&w={width}` |
| Contentful | `?w={width}&fm=webp&q=80` |
| Lab Digital DAM | `?format=auto&width={width}` |
| Bluestone PIM | `?format=auto&width={width}` |
## Deployment
[Section titled “Deployment”](#deployment)
The application builds with `output: "standalone"` for containerized deployments without `node_modules`. The build ID is set from `SERVICE_VERSION` for deterministic cache busting.
A Sentry tunnel at `/api/capture-errors` proxies browser error reports through the application’s own domain to avoid ad-blocker interference. See [Observability](/architecture/cloud/observability/) for the full error tracking setup.
[UI component library](/architecture/frontend/component-library/)The two-tier UI component library: base primitives and commerce components, with CVA variants and Radix / React Aria accessibility.
[Next.js middleware](/architecture/frontend/middleware/)The Next.js middleware chain: token refresh, B2B routing, i18n, and security headers.
# UI component library
> The two-tier UI component library: base primitives and commerce components, with CVA variants and Radix / React Aria accessibility.
The storefront’s UI lives in the `@evolve-storefront/ui` package (`frontend/packages/ui/`) and is organized into two tiers: a small set of base primitives and a larger set of commerce-specific components built on top of them.
## Architecture
[Section titled “Architecture”](#architecture)
* packages/ui/src/
* base/ \~16 primitives (Button, Input, Select, …)
* …
* components/ \~30 commerce components (ProductCard, Navigation, …)
* …
* helpers/ Utility functions (cn, typography)
* …
* global.css Design tokens + Tailwind theme (at package root)
* …
### Base primitives
[Section titled “Base primitives”](#base-primitives)
These are low-level building blocks with no commerce knowledge. They wrap Radix UI and React Aria (`react-aria-components`) for accessibility and use CVA for variant management.
`alert-dialog` `button` `checkbox` `container` `icon` `input` `label` `radio` `radio-group` `range-slider` `ribbon` `select` `skeleton` `switch` `text-area` `typography`
### Commerce components
[Section titled “Commerce components”](#commerce-components)
Higher-level components that compose base primitives with business logic. Examples include product cards, cart line items, checkout summaries, and navigation.
Product display: `product-card` `product-carousel` `product-gallery` `product-grid` `product-header` `product-list` `product-specifications` `product-stock-info` `reviews-stars`
Checkout & cart: `checkout-summary` `quantity-selector` `address-card` `delivery-methods` `line-item` `summary` `order-status-indicator` `shopping-list`
Content & data: `catalog-card` `catalog-carousel` `content-teaser` `data-table` `table` `markdown-container` `usp-list`
Navigation & layout: `accordion` `breadcrumbs` `card` `footer` `hero` `navigation`
Modals & feedback: `dialog` `toaster` `tooltip`
Variant selectors: `variant-selectors` (button, color, and image selector sub-components)
## Component patterns
[Section titled “Component patterns”](#component-patterns)
### CVA variants
[Section titled “CVA variants”](#cva-variants)
Components declare their visual variants with [Class Variance Authority](https://cva.style/docs):
```tsx
import { cva } from "class-variance-authority";
export const buttonVariants = cva(
"inline-flex cursor-pointer items-center justify-center …",
{
variants: {
variant: {
primary: "bg-orange-500 font-bold text-black …",
secondary: "bg-black font-bold text-white …",
tertiary: "text-gray-600 hover:bg-gray-50 …",
link: "text-black hover:text-gray-500 …",
outline: "bg-white font-semibold ring-2 ring-black …",
},
size: {
lg: "gap-lg px-4 py-[10px] text-sm",
xl: "gap-sm px-4 py-2.5 text-sm md:px-6 md:py-3 md:text-base",
xxl: "gap-sm px-4 py-2.5 text-sm md:px-6 md:py-3 md:text-base",
},
},
defaultVariants: { variant: "primary", size: "xxl" },
}
);
```
### Radix UI and React Aria for accessibility
[Section titled “Radix UI and React Aria for accessibility”](#radix-ui-and-react-aria-for-accessibility)
Base components use Radix UI and React Aria Components primitives for keyboard navigation, focus management, and ARIA attributes. The `asChild` pattern (via `Slot.Root`) lets you swap the rendered element while keeping behaviour:
```tsx
const Component = asChild ? Slot.Root : "button";
```
### The `cn()` helper
[Section titled “The cn() helper”](#the-cn-helper)
All components merge class names with `cn()`, which combines `clsx` (conditional classes) and `tailwind-merge` (deduplication of Tailwind utilities):
```typescript
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
```
## Theming
[Section titled “Theming”](#theming)
Design tokens are defined in two layers in `global.css`. A base layer sets raw values as CSS custom properties, and the `@theme` directive maps them into Tailwind:
```css
/* Base layer - raw design token values */
@layer base {
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(240 10% 3.9%);
--primary: hsl(240 5.9% 10%);
--destructive: hsl(0 72.22% 50.59%);
--radius: 0.5rem;
}
}
/* Tailwind theme - maps tokens to utility classes */
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-destructive: var(--destructive);
--radius-lg: var(--radius);
}
```
Dark mode is enabled through a custom Tailwind variant:
```css
@custom-variant dark (&:is(.dark *));
```
This means dark-mode styles activate when an ancestor has the `dark` class, rather than relying on `prefers-color-scheme`.
## Icons
[Section titled “Icons”](#icons)
The library uses [Lucide React](https://lucide.dev/) for icons. A custom `Icon` component renders icons as CSS masks rather than inline SVGs, reducing DOM size:
```tsx
```
## Storybook
[Section titled “Storybook”](#storybook)
All components have Storybook stories alongside their source files. A hosted instance is available at [ui.evolve.labdigital.nl](https://ui.evolve.labdigital.nl). The Storybook setup includes:
* Accessibility checks via `@storybook/addon-a11y`
* Responsive viewport presets (minimal through extra-large)
* Auto-generated documentation from component props
## Analytics
[Section titled “Analytics”](#analytics)
The `@evolve-storefront/gtm-ecommerce` package provides Google Tag Manager Enhanced Ecommerce events. Ten standard ecommerce events are implemented:
| Event | Trigger |
| ------------------- | ------------------------- |
| `view_item` | Product detail page view |
| `view_item_list` | Product listing page view |
| `select_item` | Click on a product card |
| `add_to_cart` | Add product to cart |
| `remove_from_cart` | Remove product from cart |
| `view_cart` | Cart page view |
| `begin_checkout` | Start checkout |
| `add_shipping_info` | Select shipping method |
| `add_payment_info` | Select payment method |
| `purchase` | Order confirmation |
Each event sends a standard GA4 ecommerce payload with item-level data (SKU, name, brand, category, price, quantity). The core function `sendGTMEcommerceEvent()` pushes events to the `dataLayer`.
Note
GTM event tracking needs to be wired per project. The package provides the event functions but the integration points in components are project-specific.
# Next.js middleware
> The Next.js middleware chain: token refresh, B2B routing, i18n, and security headers.
The storefront proxy (`src/proxy.ts`, Next.js 16’s successor to `middleware.ts`) runs on every navigation request and performs four tasks in sequence: token refresh, B2B route rewriting, internationalization, and security headers.
```
flowchart TD
A[Incoming request] --> B{userToken expired AND refresh cookie exists?}
B -- Yes --> C[Redirect to /account/refresh-session]
B -- No --> D{hasBusinessContext?}
D -- Yes --> E[Redirect /account/* → /account-b2b/account/*]
D -- No --> F[next-intl locale routing]
E --> F
F --> G[Set security headers]
G --> H[Response]
```
## Token refresh
[Section titled “Token refresh”](#token-refresh)
The middleware reads the JOSE header from the `userToken` cookie using `jose.decodeProtectedHeader()` and checks the `exp` claim. If the token has been expired for more than 5 minutes and a `userRefreshTokenExists` cookie is present, the request is redirected to `/account/refresh-session` which exchanges the refresh token for a new access token.
This cleans up stale sessions so server-rendered pages don’t attempt to use long-expired tokens.
## B2B routing
[Section titled “B2B routing”](#b2b-routing)
When the `userData` JWT contains a `businessContext` object (checked via `readUserData()`), the middleware redirects `/account/*` requests to `/account-b2b/account/*`. This lets B2B customers see a different account section with company management, quotes, and approval workflows.
See [B2B architecture](/architecture/b2b/) for how business context is established.
## Internationalization
[Section titled “Internationalization”](#internationalization)
The `next-intl` middleware handles locale detection and prefix routing. Every URL must include a locale segment (e.g. `/en/cart`, `/nl/winkelwagen`). The locale list and custom prefix mapping are defined in `packages/site-config/src/i18n/config.ts`.
## Security headers
[Section titled “Security headers”](#security-headers)
The `setResponseHeaders()` function from `src/lib/secure-headers.ts` adds security headers to every response:
| Header | Value |
| --------------------------- | ---------------------------------------------- |
| `Strict-Transport-Security` | `max-age=63072000; includeSubDomains; preload` |
| `X-Content-Type-Options` | `nosniff` |
| `Referrer-Policy` | `same-origin` |
| `Permissions-Policy` | Blocks camera, microphone, geolocation |
| `Content-Security-Policy` | Per-source allowlists (see below) |
### Content Security Policy
[Section titled “Content Security Policy”](#content-security-policy)
The CSP is assembled dynamically based on environment variables. Key source allowlists:
* **connect-src**: GraphQL gateway, OpenTelemetry endpoint, Sentry, Google Analytics, Adyen
* **script-src**: `self`, GTM, Stripe, Sentry, Storyblok bridge
* **frame-src**: Stripe, YouTube, Adyen checkout
* **img-src**: `self`, HTTPS, data URIs, Contentful assets
In production, `upgrade-insecure-requests` is added to the CSP directives. Sentry CSP violation reports are configured through the `Report-To` header.
## Matcher
[Section titled “Matcher”](#matcher)
The middleware intercepts all routes except API routes and static files (anything with a file extension):
```typescript
export const config = {
matcher: ["/", "/((?!api|_next|.*\\..*).*)" ],
};
```
# Infrastructure
> Cross-cutting infrastructure: the GraphQL gateway, authentication, caching, messaging, webhooks, email, and search.
This section covers the cross-cutting infrastructure that supports every domain service: the federated GraphQL gateway, authentication and tokens, caching layers, asynchronous messaging, REST webhooks, email delivery, and search integration.
[Authentication & tokens](/architecture/infrastructure/authentication/)How Evolve manages authentication tokens, sessions, and identity across the gateway and backend services.
[Caching strategy](/architecture/infrastructure/caching/)Caching strategies across the frontend, GraphQL gateway, and backend services.
[Messaging & events](/architecture/infrastructure/messaging/)How Evolve services communicate asynchronously through events and cloud-native messaging.
[REST endpoints & webhooks](/architecture/infrastructure/rest-webhooks/)How Evolve uses REST endpoints for webhooks, payment callbacks, session management, and commercetools API extensions.
[Email & notifications](/architecture/infrastructure/email/)How Evolve sends transactional emails through a two-layer notification and email rendering system.
[Search & Algolia](/architecture/infrastructure/search/)How Evolve integrates search through pluggable providers with Algolia and commercetools support.
[GraphQL Gateway](/architecture/infrastructure/gateway/)How the Hive Router gateway federates subgraph services, manages tokens, and enforces persisted documents.
# Authentication & tokens
> How Evolve manages authentication tokens, sessions, and identity across the gateway and backend services.
Evolve uses a federated token system to manage authentication across the GraphQL gateway and backend services. The gateway (Hive Router) is the single public entry point: its `federated_token` plugin (`hive-router-federated-token`, compiled into the router binary) reads and validates tokens from cookies, injects them into subgraph requests as the `x-access-token` header, and writes updated tokens back to cookies on the response. Backend services use `@labdigital/federated-token` to read and modify the token.
## Token types
[Section titled “Token types”](#token-types)
The system uses three distinct tokens, each with a different purpose and visibility:
| Token | Format | Storage | Lifetime | Readable by frontend |
| ------------- | --------------------------- | ------------------------------------------------- | -------------------------------- | -------------------- |
| Access token | JWE (encrypted JWT) | `userToken` or `guestToken` cookie (httpOnly) | 2 days | No |
| Refresh token | JWE (encrypted JWT) | `refreshToken` cookie (httpOnly, path-restricted) | 200 days (CT inactivity timeout) | No |
| Data token | JWT (signed, not encrypted) | `userData` or `guestData` cookie | 90 days | Yes |
**Access token**: holds the commercetools API access token, its expiry, the subject (`customer_id:` or `anonymous_id:`), and whether the session is authenticated. Encrypted with AES-256-GCM so the client cannot read or tamper with it.
**Refresh token**: holds the commercetools refresh token. Stored in a separate cookie with a restricted path (`/graphql/auth`) so it is only sent on refresh requests, not on every API call.
**Data token**: holds non-sensitive metadata the frontend needs without a server round-trip. The customer’s first name, last name, and B2B business context. Signed (HS256) so the frontend can read it but cannot modify it.
The frontend also receives non-httpOnly indicator cookies (`userRefreshTokenExists` / `guestRefreshTokenExists`) so JavaScript can detect whether a refresh token exists without being able to read the token itself.
## Token flow
[Section titled “Token flow”](#token-flow)
```
sequenceDiagram
actor Browser
participant GW as Gateway
participant Svc as Domain service
participant CT as commercetools
Note over Browser,CT: Authenticated request
Browser->>GW: GraphQL request + cookies
GW->>GW: Decrypt access token (JWE)
GW->>Svc: Forward request + X-Access-Token header
Svc->>CT: API call with CT access token
CT-->>Svc: Response
Svc-->>GW: Response + X-Access-Token (if modified)
GW->>GW: Create new JWE cookies if tokens changed
GW-->>Browser: Response + Set-Cookie headers
```
The gateway serializes the `FederatedToken` into a base64-encoded `X-Access-Token` header for downstream services. Refresh tokens are forwarded via `X-Refresh-Token`. Store context (store key, locale, currency) travels in separate `X-StoreContext-*` headers.
When a downstream service modifies the token (e.g., during login), it returns the updated token in `X-Access-Token` and `X-Refresh-Token` response headers. The gateway merges these changes and writes new cookies.
## The `@requiresSession` directive
[Section titled “The @requiresSession directive”](#the-requiressession-directive)
The `@requiresSession` GraphQL directive ensures a valid session exists before a field resolves. It supports two modes:
* **`@requiresSession`**: requires an existing session. Throws a `REQUIRES_SESSION` error if no token is present.
* **`@requiresSession(create: true)`**: creates an anonymous session on the fly if no token exists. Used for operations that should work without prior login, such as adding items to a cart.
When `create: true` fires and no session exists, the subgraph creates an anonymous commercetools token itself and sets it on the `FederatedToken`. The router’s `federated_token` plugin picks up the updated token from the response headers and writes the session cookies.
Fields using `@requiresSession(create: true)`:
* `cartLineItemsAdd`, `shoppingListCreate`, `shoppingListLineItemsAdd`, `createGuestSession`
Fields using `@requiresSession` (existing session required):
* `cartUpdate`, `cartLineItemsUpdate`, `cartLineItemsRemove`, `cartDiscountCodeAdd`, `cartDiscountCodeRemove`, `checkoutComplete`, `createPayment`, `shoppingList`, `shoppingLists`, and other account/order mutations
## Guest vs authenticated flows
[Section titled “Guest vs authenticated flows”](#guest-vs-authenticated-flows)
### Guest flow
[Section titled “Guest flow”](#guest-flow)
1. A visitor performs an action that requires a session (e.g., adds an item to the cart via `cartLineItemsAdd`)
2. The `@requiresSession(create: true)` directive fires
3. The subgraph requests an anonymous token from commercetools OAuth (`grant_type=client_credentials` on the anonymous endpoint) and sets it on the federated token
4. The gateway creates `guestToken` and `guestData` cookies
5. The original GraphQL operation proceeds with the new anonymous token
### Login
[Section titled “Login”](#login)
1. The customer calls the `customerLogin` mutation with email and password
2. If the customer has an existing anonymous session, the account service calls commercetools’ `me().login()` endpoint, which automatically merges any anonymous cart into the customer’s cart
3. A new customer token is obtained via the password grant
4. The gateway replaces `guestToken` → `userToken` and `guestData` → `userData` cookies
Cart merging is handled transparently by commercetools when logging in with an active anonymous session.
## Token refresh
[Section titled “Token refresh”](#token-refresh)
Access tokens expire after 2 days. The frontend detects expiry through the Next.js middleware:
1. On each request, the middleware checks if `userToken` is present and not within 5 minutes of expiry
2. If the token is expired but `userRefreshTokenExists` is set, the user is redirected to `/account/refresh-session`
3. The refresh page calls the `refreshToken` GraphQL mutation via `/graphql/auth` (the refresh token cookie’s restricted path)
4. The account service exchanges the refresh token for a new access token via commercetools OAuth (`grant_type=refresh_token`)
5. On success, the gateway writes a new `userToken` cookie
6. On failure (expired or revoked refresh token), all cookies are cleared and the user is redirected to the login page
The refresh page uses exponential backoff with up to 5 retries before giving up.
## Session REST endpoints
[Section titled “Session REST endpoints”](#session-rest-endpoints)
The account service exposes three REST endpoints for session management:
**`POST /api/create-session`**: creates an anonymous commercetools session. Returns a `FederatedToken`-wrapped response with `accessToken` and `refreshToken` (base64-encoded). REST alternative to the `createGuestSession` mutation.
**`POST /token/anonymous`**: creates an anonymous session and returns raw `TokenInfo` (`subject`, `accessToken`, `refreshToken`, `expiresAt`). Used by server-side consumers that need direct API access (e.g., the MCP server).
**`POST /api/client-context`**: accepts an `X-Access-Token` header and returns customer metadata (`id`, `givenName`, `familyName`, `email`, `customerGroupId`, `businessContext`). Results are cached in Redis for 300 seconds. Used by services that need customer information without a full GraphQL query.
# Caching strategy
> Caching strategies across the frontend, GraphQL gateway, and backend services.
Evolve applies caching at multiple layers: the Next.js frontend, the GraphQL gateway, and the backend domain services. Each layer has a different caching strategy suited to its role.
## Next.js frontend
[Section titled “Next.js frontend”](#nextjs-frontend)
### HTTP caching
[Section titled “HTTP caching”](#http-caching)
The frontend sets `Cache-Control` headers for unauthenticated requests:
```plaintext
s-maxage=900; stale-while-revalidate=2592000
```
This caches pages on the CDN for 15 minutes and serves stale responses for up to 30 days while revalidating in the background. Authenticated requests (users with a session token) bypass this cache entirely.
### Data fetching
[Section titled “Data fetching”](#data-fetching)
Server-side GraphQL requests use Next.js Incremental Static Regeneration (ISR) with a 15-minute revalidation interval:
```typescript
const data = await serverClient(GetProductDetailPage, variables, {
cache: authHeaders ? "no-cache" : "force-cache",
next: {
revalidate: 900,
tags: ["product_detail_page", slug],
},
});
```
* **Unauthenticated**: responses are cached with `force-cache` and revalidated every 15 minutes
* **Authenticated**: responses use `no-cache` to ensure customer- specific data is always fresh
* **Draft mode**: all caching is disabled for content preview
React’s `cache()` function deduplicates identical requests within a single server render, preventing multiple fetches for the same data during one page load.
### On-demand revalidation
[Section titled “On-demand revalidation”](#on-demand-revalidation)
Cached pages can be revalidated immediately using Next.js cache tags. This is useful for content updates that should be visible before the 15-minute TTL expires (e.g., a CMS publish event triggering revalidation of affected pages).
## GraphQL gateway
[Section titled “GraphQL gateway”](#graphql-gateway)
The gateway is a Rust-based Hive Router and keeps no shared response cache: it does not use Redis, and there is no Apollo-style response or APQ cache. The router caches query plans in memory within its own process. In production, clients send persisted document IDs instead of full query text, which serves the same bandwidth-saving purpose APQ did.
## Backend services
[Section titled “Backend services”](#backend-services)
### Redis caching
[Section titled “Redis caching”](#redis-caching)
Backend services use the framework’s cache module, which provides a Redis-backed cache with automatic fallback to a no-op store when Redis is unavailable:
```typescript
import { cache } from "@evolve-framework/core/cache";
await cache.configure(config.REDIS_URL);
const result = await cache.wrapFn({
key: `product:${storeContext.cacheKeyPrefix}:${slug}`,
fn: () => fetchProduct(slug),
ttl: 300_000, // 5 minutes
});
```
The `wrapFn` method handles cache lookup, miss execution, and storage in a single call with built-in tracing.
### DataLoader caching
[Section titled “DataLoader caching”](#dataloader-caching)
DataLoaders that fetch frequently accessed data (categories, breadcrumbs, content snippets) are configured with Redis-backed caching. Cache keys include the store context (store key, locale, currency) to prevent data leaking between stores:
```typescript
new DataLoaderCache(fetchFn, {
cache: {
ttl: 5 * MINUTES,
storeFn: () => cache.getStore("cms:dataloaders"),
},
cacheKeyFn: (key) =>
`snippet:${storeContext.cacheKeyPrefix}:${key}`,
});
```
### Cache namespaces
[Section titled “Cache namespaces”](#cache-namespaces)
Services use namespaced cache stores to keep cached data separated:
| Namespace | Service | Purpose |
| ----------------- | ------- | ----------------------- |
| `cms:page-cache` | CMS | Page content cache |
| `cms:dataloaders` | CMS | DataLoader result cache |
| `cms:cache` | CMS | CMS version tracking |
## Guidelines
[Section titled “Guidelines”](#guidelines)
* **Default to no caching**: add caching explicitly where it provides measurable benefit. Premature caching creates staleness bugs that are hard to debug.
* **Include the store context in cache keys**: use the `storeContext.cacheKeyPrefix` (which combines store key, locale, and currency) to prevent cross-store cache pollution.
* **Use short TTLs for commerce data**: product availability and pricing change frequently. 5 minutes is a reasonable default for catalog data.
* **Use longer TTLs for CMS content**: content changes less frequently. 15 to 60 minutes works well for page content and navigation.
* **Skip caching for customer data**: carts, orders, and account information should never be cached in shared caches.
# Email & notifications
> How Evolve sends transactional emails through a two-layer notification and email rendering system.
Evolve uses a two-layer system for transactional emails. Domain services publish notification events to a message queue through the `@evolve-framework/user-notifications` package. The `email-smtp` service consumes these events, renders emails using React Email templates, and delivers them via SMTP.
This separation means domain services never deal with email rendering or delivery. They just say “notify the customer about X” and the email service handles the rest.
## Architecture
[Section titled “Architecture”](#architecture)
```
graph LR;
svc["Domain service"] -- "publish notification" --> queue["Message queue"]
queue --> email["email-smtp service"]
email -- "render template" --> react["React Email"]
email -- "send" --> smtp["SMTP server"]
smtp --> inbox["Customer inbox"]
```
The notification client validates each event against a Zod schema before publishing, ensuring the email service always receives well-formed data. The message queue provides reliable delivery with retries and dead-letter handling.
## Notification types
[Section titled “Notification types”](#notification-types)
| Type | Published by | Variables |
| ------------------------- | ---------------- | ------------------------------------------------ |
| `account-created` | Account service | — |
| `order-confirmed` | Checkout service | Order number, customer, billing/shipping address |
| `order-shipped` | Checkout service | Shipment details |
| `password-reset-request` | Account service | Reset token, expiry |
| `password-reset-complete` | Account service | — |
All notifications carry a shared envelope:
```typescript
{
type: "order-confirmed",
origin: "checkout-commercetools",
recipients: [{ email: "customer@example.com", name: "Jane Doe" }],
storeContext: { storeKey: "main-store", locale: "en-GB" },
timestamp: "2025-01-15T10:30:00Z",
variables: { orderNumber: "ORD-2025-001", /* ... */ }
}
```
The `messageGroupId` is an MD5 hash of the sorted recipient email list, which ensures FIFO ordering per recipient where the transport supports it.
## Publishing notifications
[Section titled “Publishing notifications”](#publishing-notifications)
Domain services use `QueuedUserNotificationClient` to publish notifications:
```typescript
const notificationClient = new QueuedUserNotificationClient();
notificationClient.configure(
config.COMPONENT_NAME,
resolvePublisher(config.INTERNAL_EVENTS_TARGET),
);
await notificationClient.sendOrderConfirmation({
recipients: [{ email: customer.email, name: customer.name }],
storeContext: { storeKey, locale },
variables: { orderNumber, customer, billingAddress, shippingAddress },
});
```
Each notification type has a dedicated method that validates the payload at compile time and runtime.
## Email rendering
[Section titled “Email rendering”](#email-rendering)
The `email-smtp` service renders emails using [React Email](https://react.email/) JSX templates. Each notification type has a corresponding template:
* email-smtp/src/emails/
* account-created.tsx
* order-confirmation.tsx
* order-shipped.tsx
* password-reset-complete.tsx
* password-reset-request.tsx
The render function produces both HTML and plain-text versions in parallel using `@react-email/render`. The plain-text version automatically strips images and preview elements.
### Internationalization
[Section titled “Internationalization”](#internationalization)
Templates use a custom `useTranslations(scope, locale)` hook backed by `intl-messageformat`. Translations are stored as JSON files per locale and support ICU message format for plurals and selects:
```plaintext
Supported locales: de-DE, en-GB, es-ES, fr-BE, fr-FR, it-IT, nl-BE, nl-NL
```
Each email handler loads translations for the notification’s locale and passes the `t()` function to the template:
```typescript
const t = useTranslations("OrderConfirmation", locale);
await sendMail({
recipients,
subject: t("subject", { orderNumber }),
...render(),
});
```
## SMTP transport
[Section titled “SMTP transport”](#smtp-transport)
The email service connects to any SMTP-compatible server. Configuration is through environment variables:
| Variable | Purpose |
| ----------------- | -------------------------------- |
| `SMTP_HOST` | SMTP server hostname |
| `SMTP_PORT` | SMTP server port |
| `SMTP_SECURE` | Enable TLS |
| `SMTP_USERNAME` | SMTP authentication username |
| `SMTP_PASSWORD` | SMTP authentication password |
| `EMAIL_FROM` | Default sender address |
| `EMAIL_ALLOWLIST` | Comma-separated recipient filter |
### Allowlist filtering
[Section titled “Allowlist filtering”](#allowlist-filtering)
In non-production environments, the `EMAIL_ALLOWLIST` variable restricts delivery to matching recipients only. The filter uses domain suffix matching, so `@labdigital.nl` allows all addresses at that domain. When the allowlist is unset (production), all recipients are allowed.
This prevents accidental emails to real customers from staging or development environments.
### Local development
[Section titled “Local development”](#local-development)
For local development, Mailpit runs as a Docker container (started by `task docker:services`) and captures all outgoing email on `localhost:1025`. The Mailpit web UI at `localhost:8025` lets you inspect rendered emails without any SMTP configuration.
## Adding a new notification
[Section titled “Adding a new notification”](#adding-a-new-notification)
To add a new notification type:
1. **Define the schema**: add a new JSON Schema in `schemas/src/notifications/` with the notification’s variables
2. **Generate types**: run `pnpm codegen` to generate the Zod schema and TypeScript types
3. **Add the client method**: add a typed method to `QueuedUserNotificationClient` in the `user-notifications` package
4. **Publish from the domain service**: call the new method where the business event occurs
5. **Create the email template**: add a React Email component in `email-smtp/src/emails/`
6. **Add translations**: add translation keys for all supported locales
7. **Add the handler**: register a case in the email service’s `processNotificationEvent` switch statement
8. **Subscribe the queue**: if using a new event type, add an EventBridge rule (AWS) or equivalent subscription for the new type
# GraphQL Gateway
> How the Hive Router gateway federates subgraph services, manages tokens, and enforces persisted documents.
The GraphQL gateway is the single entry point for all frontend GraphQL traffic. It is a custom [Hive Router](https://the-guild.dev/graphql/hive/docs/router) binary written in Rust: the platform compiles its own router executable with the Evolve plugins built in (“bring your own binary” is the Hive Router plugin model). It composes the domain subgraphs into one federated supergraph and handles cross-cutting concerns like authentication, persisted-document enforcement, and observability — with no Node.js runtime in the request path.
## Stack
[Section titled “Stack”](#stack)
| Component | Source |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Hive Router | `hive-router` crate, pinned release tag (see `Cargo.toml`) |
| Federated token plugin | [`hive-router-federated-token`](https://github.com/evolve-platform/hive-router-federated-token) (own repo, compiled in) |
| Authoring token plugin | in-tree crate (upcoming, ships with the authoring stack) |
The service lives in `backend/services/graphql-gateway/`: `src/main.rs` registers the plugins against the router entrypoint, `router.config.yaml` is the production config template, and `router.config.local.yaml` is used for local development.
## Supergraph composition
[Section titled “Supergraph composition”](#supergraph-composition)
The router loads its supergraph from one of two sources:
* **Production**: the Hive CDN, polled for updates. Activated by the `HIVE_CDN_ENDPOINT` + `HIVE_CDN_KEY` environment variables (the router’s native override turns these into `supergraph.source: hive`).
* **Development**: a local file, `supergraph.generated.graphql`, composed from the subgraph SDLs with the Hive CLI:
```bash
task -d backend/services/graphql-gateway supergraph
# or continuously:
task -d backend/services/graphql-gateway supergraph:watch
```
The composition task takes a `CMS` variable (`contentful`, the default, or `storyblok`) — the two CMS subgraphs are interchangeable and only differ in which `schema.generated.graphql` fills the `cms` slot.
### Subgraphs
[Section titled “Subgraphs”](#subgraphs)
| Subgraph | Default dev URL | Domain |
| -------- | ------------------------------- | ------------------------------------- |
| account | `http://localhost:4001/graphql` | Authentication & customer data |
| catalog | `http://localhost:4002/graphql` | Products, categories, search |
| checkout | `http://localhost:4003/graphql` | Cart, orders, payments |
| cms | `http://localhost:4004/graphql` | CMS content (Contentful or Storyblok) |
| order | `http://localhost:4005/graphql` | Order history & management |
| quotes | `http://localhost:4006/graphql` | B2B quote requests |
## Endpoints
[Section titled “Endpoints”](#endpoints)
| Path | Purpose |
| --------------- | -------------------------------------------------------- |
| `/graphql` | Main GraphQL endpoint (GET/POST; serves GraphiQL in dev) |
| `/graphql/auth` | Same GraphQL endpoint on the refresh-cookie path |
The router serves any sub-path of the GraphQL endpoint, which is what makes the cookie-path separation work: the refresh-token cookie is scoped to `/graphql/auth` so it is never sent on regular queries, limiting exposure.
## Authentication
[Section titled “Authentication”](#authentication)
Customer authentication is implemented by the `federated_token` plugin, a Rust port of the `@labdigital/federated-token` Apollo plugin. The subgraph wire format is unchanged — subgraphs still receive base64-JSON `x-access-token` / `x-refresh-token` headers — so the gateway swap is transparent to every downstream service.
The plugin reads and validates the client’s tokens on each GraphQL request (cookies first, headers as fallback), injects them on outgoing subgraph requests, merges tokens minted or rotated by subgraphs back off their responses, and writes the resulting cookies on the client response. Configuration (issuer, audience, cookie domain, key rotation) lives in the `plugins.federated_token` block of the router config; the symmetric keys come from `FEDERATED_TOKEN_ENCRYPT_KEY_1` / `FEDERATED_TOKEN_SIGN_KEY_1`.
See [Authentication & tokens](/architecture/infrastructure/authentication/) for token types and the session flow.
The `authoring_token` plugin (upcoming, part of the authoring stack) resolves the editor’s CMS OAuth token from the `__authoring_session` cookie or the `X-Authoring-Cms-Token` header and forwards it to subgraphs. Unlike the customer plugin it never fails a request — enforcement lives in the CMS subgraph’s authoring resolvers.
## Persisted documents
[Section titled “Persisted documents”](#persisted-documents)
Trusted-document enforcement is native to Hive Router. In production the `persisted_documents` config block is enabled with `require_id: true`: only operations whose `documentId` is registered in the Hive CDN (App Deployments) are accepted, and arbitrary operations — including introspection — are rejected. There is no bypass header.
In development the block is disabled, because the dev storefront falls back to full queries and there is no local Hive CDN.
## Header propagation & store context
[Section titled “Header propagation & store context”](#header-propagation--store-context)
Header forwarding to subgraphs is declarative router config (the `headers:` section of `router.config.yaml`) — it replaces the custom Apollo data-source code the Node gateway used:
| Incoming header | Forwarded as | Notes |
| ------------------------- | ------------- | ----------------------------------------- |
| `X-StoreContext-StoreKey` | same | Store context, supplied by the storefront |
| `X-StoreContext-Locale` | same | |
| `X-StoreContext-Currency` | same | |
| `X-Forwarded-For` | `X-Client-IP` | Defaults to `127.0.0.1` locally |
See [Store context](/architecture/core/store-context/) for how services consume these values.
## CORS
[Section titled “CORS”](#cors)
CORS is router config (`cors:` block). Credentialed requests require explicit origins (never `*`): local dev allows `http://localhost:3000` and the Portless storefront alias; production origins are injected through the `CORS_ORIGINS` environment variable (a JSON array rendered into the config at container start).
## Observability
[Section titled “Observability”](#observability)
The router exports OpenTelemetry traces and metrics over OTLP. The rest of the stack propagates trace context via B3, and the router matches it — extracting incoming B3 context and injecting B3 toward subgraphs so spans stay in one trace. In production the exporter uses OTLP over HTTP (the collector sits behind an L7 proxy that mangles gRPC trailers); locally it exports to the docker-compose OTel Collector.
## Configuration
[Section titled “Configuration”](#configuration)
Hive Router does not interpolate `${VAR}` in its YAML. Two mechanisms fill the gap:
* **Container start**: `docker-entrypoint.sh` renders `router.config.yaml` with `envsubst` for an allowlisted set of variables (`CORS_ORIGINS`, `JWT_ISSUER`, `JWT_AUDIENCE`, `COOKIE_DOMAIN`, the token keys, the OTLP endpoint).
* **Plugin init**: the Evolve plugins expand `${VAR}` references in their own config blocks at startup, so secrets stay in the environment.
Key environment variables:
| Variable | Purpose |
| -------------------------------------------------------------- | ---------------------------------------- |
| `HIVE_CDN_ENDPOINT` / `HIVE_CDN_KEY` | Supergraph source (production) |
| `JWT_ISSUER` / `JWT_AUDIENCE` | Token claims validated by the plugins |
| `COOKIE_DOMAIN` | Cookie domain for auth tokens |
| `FEDERATED_TOKEN_ENCRYPT_KEY_1` / `FEDERATED_TOKEN_SIGN_KEY_1` | Customer token keys (32 bytes) |
| `CORS_ORIGINS` | Allowed origins (JSON array, production) |
| `ROUTER_CONFIG_FILE_PATH` | Which config file to load |
## Local development
[Section titled “Local development”](#local-development)
`pnpm dev` (or `pnpm dev:backend`) runs the gateway via `task -d backend/services/graphql-gateway dev`, which composes the supergraph and then `cargo run`s the router on port 4000 against `router.config.local.yaml`. A Rust toolchain is required. The Taskfile provides working defaults for every secret so the router boots without additional setup.
# Messaging & events
> How Evolve services communicate asynchronously through events and cloud-native messaging.
Evolve uses event-driven messaging for asynchronous communication between services. When something noteworthy happens (a product is published, a payment succeeds, an order is confirmed), the responsible service publishes an event to a message queue. Other services subscribe to the events they care about and react accordingly: updating search indexes, sending emails, or transitioning order states.
This decouples services from each other: the checkout service doesn’t need to know about the email service, it just publishes an event and moves on.
## Architecture
[Section titled “Architecture”](#architecture)
Events flow through the system in two ways:
1. **commercetools subscriptions** deliver platform events (resource changes, message types) directly to a cloud queue
2. **Internal events** are published by services to a shared event bus after processing commercetools events or handling webhooks
```
graph LR;
ct["commercetools"] -- "subscription" --> queue["Cloud queue"]
queue --> handler["Event handler"]
handler -- "process + transform" --> bus["Internal event bus"]
bus --> consumer1["Search sync"]
bus --> consumer2["Email service"]
bus --> consumer3["CMS cache invalidation"]
```
A service receives a raw commercetools event (e.g., `ResourceCreated` for a product), fetches the full resource, validates it against a Zod schema, and publishes a normalized internal event (e.g., `product-created`) to the internal event bus. Downstream consumers only deal with the clean, validated internal events.
## Publishing messages
[Section titled “Publishing messages”](#publishing-messages)
The `@evolve-packages/cloud-adapters` package provides a `resolvePublisher` factory that abstracts away the cloud provider. You give it a target URL and it returns a `MessagePublisher`:
```typescript
import { resolvePublisher } from "@evolve-packages/cloud-adapters";
const publisher = resolvePublisher(config.INTERNAL_EVENTS_TARGET);
await publisher.publish(event, {
type: event.type,
origin: "catalog-commercetools",
messageGroupId: resource.id,
});
```
The factory inspects the target URL to determine which cloud provider to use. This means the same service code runs on any cloud without modification. The target is an environment variable that differs per deployment.
Every event is validated against a Zod schema before publishing. The schemas are generated from JSON Schema definitions in the `schemas/` directory and provide both compile-time types and runtime validation.
## Provider implementations
[Section titled “Provider implementations”](#provider-implementations)
| Provider | Target format | Transport |
| ----------------- | ----------------------------------------------- | ----------------------------------------- |
| AWS EventBridge | `arn:aws:events:...` | `PutEventsCommand` |
| AWS SQS | `https://sqs.*.amazonaws.com/...` | `SendMessageCommand` |
| Azure Service Bus | `*.servicebus.windows.net` or connection string | `ServiceBusClient` |
| Azure Event Grid | `*.eventgrid.azure.net` | CloudEvent via `EventGridPublisherClient` |
| GCP Pub/Sub | `projects/*/topics/*` | `PubSub.topic().publishMessage()` |
| Redis | `redis://host/queue` | `lPush` (development only) |
All providers pass the event `type` and `origin` as message attributes (or equivalent metadata), and use `messageGroupId` for ordering guarantees where the transport supports it.
Redis is used for local development only. The `startDevelopmentListener` polls the queue with `brPop` and supports a dead-letter queue target for failed messages.
## Handling messages
[Section titled “Handling messages”](#handling-messages)
Each cloud provider has a corresponding handler factory that wraps the cloud-specific trigger (Lambda, Azure Function, Cloud Run HTTP) into a provider-agnostic callback:
```typescript
// AWS Lambda consuming from SQS
const sqsHandler = createSQSHandler(handleInternalEvent);
export const handler = lambdaHandlerFactory("event-handler", () => sqsHandler);
// Azure Function consuming from Service Bus
const serviceBusHandler = createServiceBusHandler(handleEvent);
app.serviceBusQueue("event-queue", { handler: serviceBusHandler });
// GCP Cloud Run consuming from Pub/Sub push
const pubsubHandler = createPubSubHandler(handleEvent);
```
Inside the handler, a switch on `event.type` dispatches to the appropriate business logic:
```typescript
const handleInternalEvent = async (event: CloudEventsPayload) => {
switch (event.type) {
case "com.commercetools.product.change.ResourceCreated":
return handleProductCreate(event.data);
case "com.commercetools.product.message.ProductPublished":
return handleProductPublished(event.data);
// ...
}
};
```
Failed messages are retried by the cloud transport (SQS visibility timeout, Service Bus lock duration). After the maximum retry count (typically 4 attempts), messages move to a dead-letter queue for investigation.
## Event types
[Section titled “Event types”](#event-types)
### Internal events
[Section titled “Internal events”](#internal-events)
These normalized events are published to the internal event bus after processing commercetools events:
| Event | Published by | Consumed by |
| -------------------------- | ------------ | ---------------- |
| `product-created` | Catalog | Search sync |
| `product-updated` | Catalog | Search sync |
| `product-published` | Catalog | Search sync |
| `product-unpublished` | Catalog | Search sync |
| `product-deleted` | Catalog | Search sync |
| `product-category-created` | Catalog | CMS |
| `product-category-updated` | Catalog | CMS |
| `product-category-deleted` | Catalog | CMS |
| `content-modified-event` | CMS | CDN invalidation |
| `account-created` | Account | Email |
| `order-confirmed` | Checkout | Email |
| `order-shipped` | Checkout | Email |
| `password-reset-request` | Account | Email |
| `password-reset-complete` | Account | Email |
### commercetools subscription events
[Section titled “commercetools subscription events”](#commercetools-subscription-events)
These are delivered directly from commercetools to service queues:
| Message type | Resource | Subscribed by |
| ------------------------------ | ----------------- | --------------------- |
| `ResourceCreated` | product, category | Catalog |
| `ResourceUpdated` | product, category | Catalog |
| `ResourceDeleted` | product, category | Catalog |
| `ProductPublished` | product | Catalog (search sync) |
| `ProductUnpublished` | product | Catalog (search sync) |
| `ProductDeleted` | product | Catalog (search sync) |
| `ProductPriceDiscountsSet` | product | Catalog (search sync) |
| `PaymentStatusStateTransition` | payment | Checkout |
| `OrderStateChanged` | order | Checkout, Monitoring |
| `OrderShipmentStateChanged` | order | Checkout |
## commercetools subscriptions
[Section titled “commercetools subscriptions”](#commercetools-subscriptions)
commercetools subscriptions deliver events to cloud-native queues in CloudEvents 1.0 format. Each service configures its subscriptions in Terraform:
```hcl
resource "commercetools_subscription" "internal" {
format {
type = "CloudEvents"
cloud_events_version = "1.0"
}
destination {
type = "SQS"
queue_url = aws_sqs_queue.events.id
}
changes {
resource_type_ids = ["category", "product"]
}
}
```
Subscriptions can listen for resource changes (any create, update, or delete on a resource type) or specific message types (like `ProductPublished` or `OrderStateChanged`). Each subscription targets the appropriate cloud queue for its deployment: SQS on AWS, Service Bus on Azure, Pub/Sub on GCP.
Every queue is configured with a dead-letter queue for messages that fail after the maximum number of delivery attempts.
# REST endpoints & webhooks
> How Evolve uses REST endpoints for webhooks, payment callbacks, session management, and commercetools API extensions.
Evolve services primarily expose a GraphQL subgraph for client consumption. REST endpoints are used for integrations that don’t fit the GraphQL model: webhook callbacks from external providers, session management, commercetools API extensions, and payment flows.
## When REST, when GraphQL
[Section titled “When REST, when GraphQL”](#when-rest-when-graphql)
**GraphQL** is the API for frontend clients and service-to-service queries through federation. All product, cart, account, and order operations go through GraphQL.
**REST** is used when:
* An external system pushes data in (payment webhooks, CMS webhooks)
* The protocol requires it (commercetools API extensions, OAuth token endpoints)
* The interaction is a simple request/response without the flexibility needs of GraphQL (session creation, health checks)
## Payment webhooks
[Section titled “Payment webhooks”](#payment-webhooks)
Each payment provider sends asynchronous status updates through webhook callbacks. Every provider’s webhook endpoint follows the same pattern but uses provider-specific verification:
```
sequenceDiagram
participant PSP as Payment provider
participant WH as Webhook endpoint
participant CT as commercetools
participant Bus as Event bus
PSP->>WH: POST /push (status update)
WH->>WH: Verify signature
WH->>CT: Update transaction status
CT-->>WH: Confirmation
CT->>Bus: PaymentStatusStateTransition event
WH-->>PSP: 200 OK / 202 Accepted
```
### Verification per provider
[Section titled “Verification per provider”](#verification-per-provider)
* Adyen
**Method:** HMAC
Validates each `notificationItem` with `hmacValidator.validateHMAC()` using `ADYEN_HMAC_KEY`. Returns `202 Accepted` on success, `401` on invalid HMAC.
Adyen decouples webhook acknowledgement from processing. The webhook handler publishes each notification to an internal queue and returns `202` immediately. A separate consumer processes notifications and updates commercetools. This prevents Adyen from timing out and retrying when commercetools mutations are slow.
* Stripe
**Method:** Signature
Uses `stripe.webhooks.constructEvent(rawBody, sig, STRIPE_WEBHOOK_SECRET)` with the `stripe-signature` header. Requires raw body access (Fastify `rawBody` plugin).
* Buckaroo
**Method:** HMAC
Uses the Buckaroo SDK’s `ReplyHandler` to validate the `Authorization` header against the canonical URL. Reconstructs the URL from `SERVER_URL` config since the service runs behind a reverse proxy.
* Mollie
**Method:** Fetch-and-verify
Mollie webhooks carry only a payment ID, so there is no signature to check. The handler looks up the commercetools Payment by `interfaceId`, then re-fetches the payment status from Mollie’s API rather than trusting the webhook payload. The endpoint returns `TRUE` (plain text) on success.
* PayNL
**Method:** HMAC
Custom preHandler reads `signature-method`, `signature-algorithm`, and `signature` headers. Computes `crypto.createHmac(algorithm, secret).update(rawBody).digest("hex")` and compares.
PayNL re-fetches the payment status from PayNL’s API after webhook verification rather than trusting the webhook payload directly. This prevents a stale retry from overwriting a more recent status. The endpoint returns `TRUE` (plain text) on success.
The Invoice provider (pay on account) is fully offline: there is no PSP to call back, so it exposes no webhook endpoint.
## Session endpoints
[Section titled “Session endpoints”](#session-endpoints)
The account service exposes REST endpoints for session management. These are documented in detail in [Authentication & tokens](/architecture/infrastructure/authentication/#session-rest-endpoints):
* **`POST /api/create-session`**: creates an anonymous session (REST alternative to the `createGuestSession` mutation)
* **`POST /token/anonymous`**: creates an anonymous session with raw token info (used by server-side consumers)
* **`POST /api/client-context`**: returns customer metadata for a given access token (cached in Redis)
## commercetools API extensions
[Section titled “commercetools API extensions”](#commercetools-api-extensions)
commercetools API extensions are synchronous HTTP callbacks that fire when resources are created or updated. They allow services to modify resources during the API call, for example setting defaults on a new cart or generating an order number.
The checkout service registers extensions for:
| Resource | Action | Behavior |
| -------- | ------ | ----------------------------------------------------------------------------------------------- |
| Cart | Create | Copies customer addresses and email onto the cart. For B2B, uses the business unit’s addresses. |
| Cart | Update | Auto-selects a default shipping method when a shipping address is set. |
| Order | Create | Generates a unique order number, ensures customer email is set, applies default order type. |
| Payment | Create | Transitions the payment to the `PaymentInitial` state. |
Extensions are registered through Terraform with a shared secret for authentication:
```hcl
resource "commercetools_api_extension" "main" {
destination {
type = "HTTP"
url = "${api_gateway_hostname}/${service_name}/extension"
authorization_header = "Bearer ${random_password.api_extension_secret.result}"
}
trigger {
resource_type_id = "cart"
actions = ["Create", "Update"]
}
trigger {
resource_type_id = "order"
actions = ["Create"]
}
trigger {
resource_type_id = "payment"
actions = ["Create"]
}
}
```
The service validates the `Authorization: Bearer ` header on every extension request. The handler dispatches based on the resource type and action, then returns update actions or errors:
```typescript
const handlers = {
cart: {
Create: createCartExtension,
Update: updateCartExtension,
},
order: {
Create: createOrderExtension,
},
payment: {
Create: (payment) => ({
actions: [{ action: "transitionState", state: { typeId: "state", key: "PaymentInitial" } }],
}),
},
};
```
Because API extensions are synchronous, they add latency to the commercetools API call. Keep extension logic fast and avoid external API calls where possible.
# Search & Algolia
> How Evolve integrates search through pluggable providers with Algolia and commercetools support.
Evolve supports pluggable search providers behind a unified GraphQL interface. The catalog service resolves `productSearch` queries by delegating to the configured provider, either Algolia for dedicated search infrastructure or commercetools’ built-in Product Search API.
## Pluggable providers
[Section titled “Pluggable providers”](#pluggable-providers)
The search provider is selected through the `SEARCH_ENGINE` environment variable:
```typescript
if (searchEngine === "algolia") {
return new AlgoliaSearch().searchProducts(context, args);
}
if (searchEngine === "commercetools") {
return new CommercetoolsSearch().searchProducts(context, args);
}
```
Both providers implement the same `SearchProvider` base class, so the GraphQL schema and frontend code are identical regardless of which provider is active. Switching providers is a configuration change, not a code change.
**Algolia** offers faster search responses, typo tolerance, synonym support, and configurable relevance ranking. It requires keeping a separate search index in sync with commercetools.
**commercetools** uses the commerce engine’s built-in search with no additional infrastructure. It supports full-text search and faceting but with less control over relevance tuning.
## Algolia index structure
[Section titled “Algolia index structure”](#algolia-index-structure)
### Index naming
[Section titled “Index naming”](#index-naming)
Each store gets its own set of Algolia indexes. The naming convention is:
```plaintext
{storeKey}_default # Default relevance sort
{storeKey}_price_asc # Price ascending
{storeKey}_price_desc # Price descending
{storeKey}_suggestions # Query suggestions
```
Store keys are lowercased. Sort-specific indexes are Algolia replicas that share the same data but apply different ranking strategies.
### Record shape
[Section titled “Record shape”](#record-shape)
Each Algolia record represents a product with all its variants:
```typescript
{
objectID: "product-id",
name: { "en-GB": "Running Shoe", "nl-NL": "Hardloopschoen" },
slug: { "en-GB": "running-shoe", "nl-NL": "hardloopschoen" },
description: { "en-GB": "...", "nl-NL": "..." },
image: "https://cdn.example.com/product.jpg",
startingPrice: { currency: "EUR", centAmount: 9999 },
categoryPageId: ["shoes", "shoes > running"],
hierarchicalCategories: {
lvl0: "Shoes",
lvl1: "Shoes > Running",
},
variants: [
{
sku: "SHOE-42-BLACK",
price: { currency: "EUR", centAmount: 9999 },
attributes: { size: "42", color: "black" },
image: "https://cdn.example.com/shoe-black.jpg",
}
]
}
```
Only variants with a price in the store’s currency are included. Localized fields only contain the languages configured for the store. Hierarchical categories enable Algolia’s built-in hierarchical faceting.
## Data synchronization
[Section titled “Data synchronization”](#data-synchronization)
Product data reaches Algolia through two paths:
```
graph TD;
ct["commercetools"] -- "subscription events" --> catalog["Catalog service"]
catalog -- "real-time update" --> algolia["Algolia"]
job["Full sync job"] -- "batch upsert" --> algolia
job -- "paginate products" --> ct
```
### Full batch sync
[Section titled “Full batch sync”](#full-batch-sync)
The `sync-algolia` job iterates all products in commercetools using cursor-based pagination, converts each to an Algolia record, and calls `saveObjects()` in bulk. Products without a price in the store’s currency are deleted from the index. Run this job after initial setup or to recover from drift.
### Real-time event-driven updates
[Section titled “Real-time event-driven updates”](#real-time-event-driven-updates)
The catalog service subscribes to commercetools product events through the [messaging system](/architecture/infrastructure/messaging/):
| Event | Action |
| -------------------------- | ----------------------------------------------------------- |
| `ProductPublished` | Re-fetch product, upsert record with `partialUpdateObjects` |
| `ProductPriceDiscountsSet` | Re-fetch product, upsert record |
| `ProductUnpublished` | Delete record from all store indexes |
| `ProductDeleted` | Delete record from all store indexes |
Updates always re-fetch the full product from commercetools rather than relying on the event payload. This ensures the Algolia record reflects the latest state even if events arrive out of order.
## GraphQL search integration
[Section titled “GraphQL search integration”](#graphql-search-integration)
The `productSearch` query is the primary search interface:
```graphql
query {
productSearch(
searchTerm: "running shoes"
filters: [
{ key: "color", selections: ["black", "blue"] }
{ key: "price", min: 50, max: 150 }
]
sort: priceAscending
page: 1
pageSize: 24
) {
total
results {
name
slug
variants { sku, price { gross { centAmount, currencyCode } } }
}
facets {
key
label
... on OptionsFacet {
options { key, label, count, selected }
}
... on RangeFacet {
min
max
selectedMin
selectedMax
}
}
}
}
```
### Multi-query pattern (Algolia)
[Section titled “Multi-query pattern (Algolia)”](#multi-query-pattern-algolia)
When using Algolia, the search executes multiple queries in a single API call. The first query returns the matching products. Each active facet filter generates an additional query with all *other* filters applied. This returns the full set of facet options for that specific facet, even when the search is filtered. This provides the “X results available if you remove this filter” counts that users expect.
## Facets
[Section titled “Facets”](#facets)
Facets are returned as part of the search response. Two types are supported:
### Options facets
[Section titled “Options facets”](#options-facets)
String or boolean attributes with discrete values. Each option includes a count of matching products:
```graphql
... on OptionsFacet {
key # "color"
label # "Color"
options {
key # "black"
label # "Black"
count # 42
selected # true/false
}
}
```
### Range facets
[Section titled “Range facets”](#range-facets)
Numeric attributes (typically price) with min/max boundaries:
```graphql
... on RangeFacet {
key # "price"
label # "Price"
min # 10.00
max # 500.00
selectedMin # 50.00 (current filter)
selectedMax # 150.00 (current filter)
}
```
Price values are converted from centAmounts to currency units (divided by 100) in the facet response.
### Facet configuration (commercetools)
[Section titled “Facet configuration (commercetools)”](#facet-configuration-commercetools)
When using the commercetools search provider, facet configuration can be stored per category in a custom field (`facetConfig`). This allows different categories to show different facets. When no category-level configuration exists, a default schema is used.
# MACH integrations
Evolve offers many integrations with a wide range of SAAS services. Evolve is built to be independent from any SAAS service and is therefore easy to integrate, in a way that doesn’t require changes to the front-end or overall architecture. It is simply an extension to the platform.
We have four categories of integrations:
* **Full**: implemented and in production with Evolve
* **Beta**: in production, but needs some work to integrate into Evolve
* **In development**: a working implementation exists and is being ported into the framework
* **Roadmap**: planned for one of our projects
## Integration overview
[Section titled “Integration overview”](#integration-overview)
Below is an overview table of what’s included (Full and Beta support) and what’s coming (Roadmap).
| SAAS provider | Category | Evolve integration status |
| -------------------------------- | -------------------- | ------------------------- |
| Adyen | Payment | Full |
| Akeneo | PIM | Beta |
| Algolia | Search | Full |
| Amazon Marketplaces | Channel integration | Beta |
| Amplience | CMS | Beta |
| Auth0 | Auth | Beta |
| Avalara | Tax | Beta |
| AWS | Cloud hosting | Full |
| AWS ServiceLense | Observability | Full |
| Azure | Cloud hosting | Full |
| Big commerce | Commerce engine | Beta |
| Bluestone PIM | PIM | Full |
| Braintree | Payment | Beta |
| Buckaroo | Payment | Full |
| Bynder | DAM | Roadmap |
| Channable | Channel integration | Beta |
| Channel Engine | Channel integration | Beta |
| Cloudinary | DAM | Full |
| commerce Layer | Commerce engine | Full |
| commercetools | Commerce engine | Full |
| commercetools search | Search | Full |
| Contentful | CMS | Full |
| Copernica | Marketing automation | Beta |
| Emporix | Commerce engine | Roadmap |
| GCP | Cloud hosting | Full |
| Google Analytics | Analytics | Full |
| Honeycomb | Observability | Full |
| Klarna | Payment | Beta |
| Klevu | Search | Roadmap |
| Kubernetes | Platform development | Roadmap |
| Luigi’s Box | Search | Beta |
| MACH composer | Platform development | Full |
| Netlify | Cloud hosting | Roadmap |
| OpenTelemetry compatible service | Observability | Full |
| Paazl | Shipping | Beta |
| Pay.nl | Payment | Full |
| Paypal | Payment | Through PSPs |
| SalesManego | Marketing automation | Beta |
| Salesforce Commerce Cloud | Commerce engine | Beta |
| Sanity | CMS | Roadmap |
| SAP Commerce Cloud | Commerce engine | In development |
| Segment | CDP | Beta |
| Sentry | Observability | Full |
| Shopify Plus | Commerce engine | Roadmap |
| Storyblok | CMS | Full |
| Stripe | Payment | Beta |
| Terraform | Platform development | Full |
| Voyado | Loyalty | Beta |
| Vercel | Cloud hosting | Full |
| Vertex | Tax | Beta |
| Wagtail | CMS | Beta |
# Model Context Protocol (MCP)
> Evolve's Model Context Protocol (MCP) capabilities for exposing platform functionality to AI agents.
Evolve includes built-in support for the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP), an open standard for connecting AI assistants to external tools and data sources. Through MCP, Evolve exposes its e-commerce capabilities as tools that AI agents can discover and invoke. This enables conversational commerce, AI-powered customer support, and autonomous workflows.
## From GraphQL to AI tools
[Section titled “From GraphQL to AI tools”](#from-graphql-to-ai-tools)
The core idea behind Evolve’s MCP implementation is simple: if you can write a GraphQL query, you can create an AI tool. Tools are defined declaratively using GraphQL directives, and everything else (input schemas, type mappings, persisted queries) is generated automatically.
### Defining a tool
[Section titled “Defining a tool”](#defining-a-tool)
Annotate any GraphQL operation with `@mcpTool` and its variables with `@mcpToolVariable`:
```graphql
query GetProducts(
$query: String @mcpToolVariable(description: "Search query for products")
$limit: Int @mcpToolVariable(description: "Number of results to return")
$offset: Int @mcpToolVariable(description: "Pagination offset")
) @mcpTool(description: "Search for products by query, with pagination") {
products(query: $query, limit: $limit, offset: $offset) {
items {
name
slug
price {
centAmount
currencyCode
}
}
total
}
}
```
### What gets generated
[Section titled “What gets generated”](#what-gets-generated)
Running `pnpm codegen` turns these annotated operations into fully typed MCP tool definitions with JSON Schema inputs and persisted document IDs:
```typescript
export const generatedTools: GeneratedMCPTool[] = [
{
name: "get_products",
description: "Search for products by query, with pagination",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query for products" },
limit: { type: "integer", description: "Number of results to return" },
offset: { type: "integer", description: "Pagination offset" },
},
},
documentId: "sha256:abc123...",
queryString: "query GetProducts($query: String, ...) { ... }",
},
];
```
No hand-written tool definitions, no manual schema maintenance. Add a new operation, run codegen, and the tool is available to any MCP client.
## How it works
[Section titled “How it works”](#how-it-works)
MCP servers in Evolve act as a bridge between AI clients and the GraphQL Federation layer. Because the tools execute GraphQL operations against the same gateway that powers your storefront and mobile app, AI clients get exactly the same data, prices, and business rules as every other channel.
```
graph TD;
claude-desktop["Claude Desktop"]-->mcp-server
ai-assistant["AI Assistant"]-->mcp-server
cli-tool["CLI Tool"]-->mcp-server
mcp-server["MCP Server"]-->graphql-gateway["GraphQL Gateway"]
graphql-gateway-->catalog["Catalog"]
graphql-gateway-->checkout["Checkout"]
graphql-gateway-->account["Account"]
graphql-gateway-->orders["Orders"]
graphql-gateway-->cms["CMS"]
```
## Key capabilities
[Section titled “Key capabilities”](#key-capabilities)
* **Query security**: tools execute as persisted documents registered with GraphQL Hive. The gateway rejects any unregistered query, so the AI can only run pre-approved operations, the same security model as the storefront. See [query security](/architecture/mcp/architecture/#query-security--persisted-documents).
* **Plugin architecture**: intercept and customize tool behavior with plugins, for example to handle authentication flows.
* **Dual transport**: serve tools over HTTP (Streamable HTTP/SSE) for web clients or stdio for desktop clients like Claude Desktop.
* **Session management**: per-session state with authentication context, store configuration, and automatic token refresh.
* **Production-ready**: rate limiting, OpenTelemetry metrics, structured logging, and health checks out of the box.
## Packages
[Section titled “Packages”](#packages)
| Package | Description |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@evolve-framework/mcp-core` | Core library for building MCP servers on top of Evolve’s GraphQL layer |
| `@evolve-platform/mcp-customer` | Reference implementation: customer-facing MCP server with product, cart, order, and account tools. Ships with Evolve as a starting point for your own MCP services. |
[Architecture](/architecture/mcp/architecture/)Architecture of Evolve's MCP implementation, covering transports, sessions, plugins, and request flow.
[Creating an MCP service](/architecture/mcp/creating-a-service/)How to create a new MCP service in Evolve using mcp-core.
[mcp-core](/architecture/mcp/mcp-core/)The @evolve-framework/mcp-core package, the foundation for building MCP servers in Evolve.
[mcp-customer](/architecture/mcp/mcp-customer/)The mcp-customer service, a customer-facing MCP server exposing product, cart, order, and account tools.
[Why MCP on top of GraphQL Federation?](/architecture/mcp/why-graphql-federation/)Why Evolve builds MCP on top of GraphQL Federation instead of connecting AI directly to vendor APIs.
# Architecture
> Architecture of Evolve's MCP implementation, covering transports, sessions, plugins, and request flow.
Evolve’s MCP implementation follows a layered architecture where MCP servers sit between AI clients and the GraphQL Federation gateway. The `@evolve-framework/mcp-core` package provides all the building blocks, while individual services like `mcp-customer` compose them into a running server.
## System overview
[Section titled “System overview”](#system-overview)
```
graph TD;
subgraph clients["MCP Clients"]
claude["Claude Desktop (stdio)"]
web["Web AI Assistant (HTTP)"]
custom["Custom Client (HTTP)"]
end
subgraph mcp-server["MCP Server"]
transport["Transport Layer"]
sessions["Session Manager"]
handlers["Tool Handlers"]
plugins["Plugins"]
auth["Auth & Token Management"]
graphql-client["GraphQL Client"]
end
claude-->|stdio|transport
web-->|HTTP SSE|transport
custom-->|HTTP SSE|transport
transport-->sessions
sessions-->handlers
handlers-->plugins
handlers-->graphql-client
plugins-->graphql-client
auth-->sessions
auth-->graphql-client
graphql-client-->gateway["GraphQL Gateway"]
gateway-->services["Backend Services"]
```
## Transport layer
[Section titled “Transport layer”](#transport-layer)
MCP servers support two transport mechanisms:
**HTTP Streamable (SSE)** for web-based and remote clients. The server exposes a single `/mcp` endpoint that handles:
* `POST /mcp` for client-to-server JSON-RPC messages
* `GET /mcp` for server-to-client event streams (SSE) — declined with `405` in JSON-response mode, see below
* `DELETE /mcp` for session termination
**stdio** for local clients like Claude Desktop. Uses standard input/output streams with the same JSON-RPC protocol.
Both transports are interchangeable. The same tools and plugins work regardless of which transport is used.
### JSON-response mode and trusted-proxy auth
[Section titled “JSON-response mode and trusted-proxy auth”](#json-response-mode-and-trusted-proxy-auth)
The HTTP transport runs in JSON-response mode (`enableJsonResponse: true`): `POST /mcp` replies with a single JSON body instead of an SSE stream, because buffering reverse proxies (e.g. Azure Container Apps ingress) stall SSE streams on their idle timeout. Consequently `GET /mcp` is declined with `405 Method Not Allowed` per the MCP spec (“server does not offer an SSE stream”) and clients fall back to POST-only request/response. When `PROXY_AUTH_ENABLED` is set, a trusted proxy (such as a chat backend) can forward a pre-authenticated session via the `X-Access-Token` / `X-Data-Token` / `X-Refresh-Token` headers, which take precedence over `Authorization: Bearer` and skip JWT pre-validation — token validity is still enforced by the downstream gateway.
## Session management
[Section titled “Session management”](#session-management)
Each client connection creates a session with isolated state:
* **Session ID**: unique identifier for the connection
* **Authentication context**: access token, refresh token, and data token
* **Store context**: locale, currency, store key, and customer group
* **Client tracking**: which client connected and when
Sessions are managed in-memory with atomic operations. The session manager handles lifecycle events (creation, updates, termination) and emits telemetry.
## Request flow
[Section titled “Request flow”](#request-flow)
When an AI client invokes a tool, the following happens:
```
sequenceDiagram
participant Client as MCP Client
participant Server as MCP Server
participant Plugin as Plugins
participant GQL as GraphQL Client
participant Gateway as GraphQL Gateway
Client->>Server: tools/call (JSON-RPC)
Server->>Server: Rate limit check
Server->>Server: Resolve session context
alt Tool intercepted by plugin
Server->>Plugin: handleTool()
Plugin->>GQL: Execute GraphQL
GQL->>Gateway: HTTP POST /graphql
Gateway-->>GQL: Response + tokens
Plugin-->>Server: Tool result
else Normal execution
Server->>GQL: Execute GraphQL
GQL->>Gateway: HTTP POST /graphql
Gateway-->>GQL: Response + tokens
GQL-->>Server: Tool result
end
Server->>Server: Update session tokens
Server->>Server: Record telemetry
Server-->>Client: CallToolResult
```
1. The client sends a `tools/call` JSON-RPC request with the tool name and arguments.
2. The server checks rate limits and resolves the session context.
3. If a plugin intercepts the tool (e.g., for authentication), the plugin handles execution with custom logic.
4. Otherwise, the tool’s GraphQL operation is sent to the gateway using persisted document IDs.
5. Response tokens (access, refresh, data) are extracted and stored in the session.
6. The tool result is returned to the client.
## Plugin system
[Section titled “Plugin system”](#plugin-system)
Plugins can intercept specific tool calls to implement custom logic. This is used for flows that need more than a simple GraphQL query, such as authentication.
A plugin declares which tools it intercepts and provides a handler:
```typescript
const plugin: MCPPlugin = {
name: "my-plugin",
version: "1.0.0",
interceptTools: ["tool_name"],
async handleTool(toolName, args, context, setSessionAuth) {
// Custom logic here
return { content: [{ type: "text", text: "result" }] };
},
};
```
The built-in **customer-auth plugin** intercepts `customer_login` and `customer_logout` to manage session transitions between guest and authenticated states.
## Authentication & tokens
[Section titled “Authentication & tokens”](#authentication--tokens)
MCP servers manage a multi-token authentication system:
* **Access token**: short-lived JWT for API authorization
* **Refresh token**: used to obtain new access tokens
* **Data token**: optional token carrying additional claims
Token refresh happens automatically when a token is within a configurable threshold of expiry (default: 5 minutes). The server pre-validates JWTs (structure, expiry, issuer, audience) before forwarding to the gateway, providing defense-in-depth.
## Query security & persisted documents
[Section titled “Query security & persisted documents”](#query-security--persisted-documents)
MCP tools execute GraphQL operations against the same gateway as the storefront. To prevent AI clients from executing arbitrary queries, the system uses **persisted documents** as an allowlist.
### How it works
[Section titled “How it works”](#how-it-works)
The codegen pipeline generates `persisted-documents.json` alongside the tool definitions. This file maps document IDs (SHA-256 hashes of the query string) to the actual GraphQL operations:
```json
{
"f8567d7e...": "query GetProducts($filters: ...) { productSearch(...) { ... } }",
"8a7edd50...": "mutation AddToCart($cartId: ID!, ...) { cartAddLineItems(...) { ... } }"
}
```
These document IDs are registered with [GraphQL Hive](https://the-guild.dev/graphql/hive)’s persisted documents feature at deploy time. At runtime, the MCP server sends only the `documentId` instead of the full query string:
```typescript
// The GraphQL executor sends the persisted document ID
body: JSON.stringify({ documentId, variables })
```
The gateway rejects any request with an unregistered document ID. This means the AI can only execute the exact queries that were defined in `operations.graphql` and approved at build time. It cannot construct arbitrary queries, access fields not included in the selection sets, or bypass the operation boundaries you’ve defined.
### Same security model as the storefront
[Section titled “Same security model as the storefront”](#same-security-model-as-the-storefront)
This is not MCP-specific. The storefront frontend uses the same persisted documents mechanism. Both the AI and the web client operate under identical constraints: only pre-registered queries execute, and the gateway rejects everything else.
Schema changes that break an MCP tool’s query are caught in CI when persisted documents are validated against the current schema. If a field is removed or a type changes, the build fails before it reaches production.
### Selective field exposure
[Section titled “Selective field exposure”](#selective-field-exposure)
Beyond the allowlist, you control what data the AI sees through the GraphQL selection sets in `operations.graphql`. A `GetProducts` tool for the AI doesn’t need to include internal pricing tiers, margin data, or supplier information: just don’t select those fields. The operation itself is the contract with the AI.
### The `exclude` flag
[Section titled “The exclude flag”](#the-exclude-flag)
Some operations need to exist in the codebase (for token refresh or session management) but should not be exposed as tools to the AI:
```graphql
mutation RefreshToken
@mcpTool(description: "Internal token refresh", exclude: true) {
refreshToken { accessToken }
}
```
Operations with `exclude: true` are included in the persisted documents (so they can be executed at runtime by the plugin system) but are omitted from the generated tool definitions. The AI never sees them.
## Observability
[Section titled “Observability”](#observability)
MCP servers emit OpenTelemetry metrics for monitoring:
| Metric | Type | Description |
| ---------------------- | ------------- | ----------------------------- |
| `mcp.tool.invocations` | Counter | Tool calls by name and status |
| `mcp.tool.duration` | Histogram | Tool execution time in ms |
| `mcp.sessions.active` | UpDownCounter | Currently active sessions |
| `mcp.sessions.created` | Counter | Total sessions created |
| `mcp.sessions.closed` | Counter | Sessions closed by reason |
Structured logging via pino provides per-session context for debugging.
# Creating an MCP service
> How to create a new MCP service in Evolve using mcp-core.
This guide walks through creating a new MCP service using `@evolve-framework/mcp-core`. The `mcp-customer` service serves as the reference implementation.
Note
This guide assumes that `@evolve-framework/mcp-core` is already installed in your project. It is included in the Evolve platform by default.
## 1. Scaffold the service
[Section titled “1. Scaffold the service”](#1-scaffold-the-service)
Start by copying the existing `mcp-customer` service as a template:
```bash
cp -r backend/services/mcp-customer backend/services/mcp-my-service
```
Update `package.json` with your service name:
```json
{
"name": "@evolve-platform/mcp-my-service",
"version": "1.0.0",
"description": "MCP server for my use case"
}
```
## 2. Configure the service
[Section titled “2. Configure the service”](#2-configure-the-service)
Update `src/config.ts` to set the component name and port:
```typescript
import { MCPCoreConfig } from "@evolve-framework/mcp-core";
export class Config extends MCPCoreConfig {
override readonly COMPONENT_NAME = "mcp-my-service";
override readonly HTTP_PORT = 6001; // Use a unique port
}
```
Pick a port that is free in your project (the reference `mcp-customer` service uses 4050).
## 3. Define your tools
[Section titled “3. Define your tools”](#3-define-your-tools)
Create or update `operations.graphql` with your GraphQL operations. Each operation that should become an MCP tool needs the `@mcpTool` directive. These operations must correspond to the supergraph schema: you can use fewer fields than the supergraph exposes, but you cannot query fields that don’t exist in it. During code generation, your operations are validated against the supergraph.
```graphql
query GetProductDetailPage(
$slug: String! @mcpToolVariable(description: "The product slug to look up")
$skuId: String @mcpToolVariable(description: "Optional specific SKU ID")
) @mcpTool(description: "Get detailed product information by its URL slug") {
product(slug: $slug, skuId: $skuId) {
name
description
slug
variants {
sku
name
price {
centAmount
currencyCode
}
images {
url
}
availability {
isOnStock
}
}
}
}
```
Note
Every variable should have an `@mcpToolVariable(description: ...)` annotation. These descriptions are what AI clients see when deciding how to call the tool. Clear, specific descriptions lead to better tool usage.
## 4. Configure code generation
[Section titled “4. Configure code generation”](#4-configure-code-generation)
Codegen runs in two passes. The first pass (`codegen.ts`) produces typed documents and persisted-document hashes; the second pass (`codegen.tools.ts`) turns the persisted-documents output into MCP tool definitions. Both validate your operations against the federation gateway’s composed supergraph plus mcp-core’s `directives.graphql` (which declares `@mcpTool`):
```typescript
// codegen.ts — pass 1: typed documents + persisted-document hashes
import { mcpToolTransform } from "@evolve-framework/mcp-core/codegen";
import type { CodegenConfig } from "@graphql-codegen/cli";
export const documentPaths = ["./operations.graphql"];
export const schemaPaths = [
"../graphql-gateway/supergraph.generated.graphql",
"./node_modules/@evolve-framework/mcp-core/directives.graphql",
];
const config: CodegenConfig = {
schema: schemaPaths,
generates: {
"./generated/": {
documents: documentPaths,
preset: "client",
documentTransforms: [mcpToolTransform],
presetConfig: {
fragmentMasking: false,
persistedDocuments: {
mode: "embedHashInDocument",
hashPropertyName: "documentId",
},
},
},
},
};
export default config;
```
```typescript
// codegen.tools.ts — pass 2: persisted documents → MCP tool definitions
import type { CodegenConfig } from "@graphql-codegen/cli";
import { documentPaths, schemaPaths } from "./codegen.ts";
const config: CodegenConfig = {
schema: schemaPaths,
documents: documentPaths,
generates: {
"./generated/mcp-tools.generated.ts": {
plugins: ["@evolve-framework/mcp-core/codegen/tools"],
config: {
persistedDocumentsPath: "./generated/persisted-documents.json",
},
},
},
};
export default config;
```
The `codegen` script in `package.json` chains the two passes (plus a validation step):
```json
{
"scripts": {
"codegen": "pnpm codegen:queries && pnpm codegen:tools && pnpm validate:persisted-documents",
"codegen:queries": "graphql-codegen --config codegen.ts",
"codegen:tools": "graphql-codegen --config codegen.tools.ts"
}
}
```
## 5. Set up the server
[Section titled “5. Set up the server”](#5-set-up-the-server)
In `src/create-server.ts`, compose the server from mcp-core building blocks:
```typescript
import {
createToolFromGenerated,
registerTools,
createCustomerAuthPlugin,
} from "@evolve-framework/mcp-core";
export async function createServer(config: Config) {
// Load generated tools
const generatedTools = await loadGeneratedTools(
"#generated/mcp-tools.generated.ts"
);
const tools = generatedTools.map(createToolFromGenerated);
// Set up plugins (optional)
const customerAuthPlugin = createCustomerAuthPlugin(
graphqlClient,
tokenConfig,
generatedTools
);
// Register tools with the MCP server
registerTools({
mcp,
tools,
graphqlClient,
getSessionContext,
setSessionAuth,
plugins: [customerAuthPlugin],
ensureRefreshTokens,
createGuestSession,
});
}
```
## 6. Generate and run
[Section titled “6. Generate and run”](#6-generate-and-run)
```bash
# Generate tool definitions from GraphQL operations
pnpm codegen
# Start with HTTP transport (for web clients)
pnpm dev
# Or start with stdio transport (for Claude Desktop)
pnpm dev:stdio
```
## Testing your tools
[Section titled “Testing your tools”](#testing-your-tools)
You can test your MCP server using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
```bash
npx @modelcontextprotocol/inspector http://localhost:6001/mcp
```
Or connect it to Claude Desktop by adding it to your Claude Desktop configuration:
```json
{
"mcpServers": {
"evolve": {
"command": "node",
"args": ["backend/services/mcp-my-service/dist/server.js"],
"transport": "stdio"
}
}
}
```
# mcp-core
> The @evolve-framework/mcp-core package, the foundation for building MCP servers in Evolve.
`@evolve-framework/mcp-core` is the core library for building MCP servers that expose Evolve’s GraphQL operations as AI-consumable tools. It handles transport, sessions, authentication, code generation, and plugin orchestration.
## Package entry points
[Section titled “Package entry points”](#package-entry-points)
The package provides several entry points for different use cases:
| Entry point | Purpose |
| ------------------------------------------------------ | --------------------------------------------- |
| `@evolve-framework/mcp-core` | Main exports: server, sessions, config, types |
| `@evolve-framework/mcp-core/codegen` | Code generation pipeline |
| `@evolve-framework/mcp-core/codegen/tools` | GraphQL-to-MCP tool generation plugin |
| `@evolve-framework/mcp-core/codegen/persisted-queries` | Persisted document generation |
| `@evolve-framework/mcp-core/plugins` | Built-in plugins (customer-auth) |
## Code generation
[Section titled “Code generation”](#code-generation)
The core of mcp-core’s developer experience is the code generation pipeline. You define tools using GraphQL directives, and the codegen generates TypeScript tool definitions with JSON schemas automatically.
### GraphQL directives
[Section titled “GraphQL directives”](#graphql-directives)
Two directives control how operations become MCP tools:
```graphql
# Mark an operation as an MCP tool
directive @mcpTool(description: String!) on QUERY | MUTATION
# Describe a variable for the tool's input schema
directive @mcpToolVariable(description: String) on VARIABLE_DEFINITION
```
### Defining a tool
[Section titled “Defining a tool”](#defining-a-tool)
A tool is a GraphQL operation annotated with `@mcpTool`:
```graphql
query GetProducts(
$query: String @mcpToolVariable(description: "Search query for products")
$limit: Int @mcpToolVariable(description: "Number of results to return")
$offset: Int @mcpToolVariable(description: "Pagination offset")
) @mcpTool(description: "Search for products by query, with pagination") {
products(query: $query, limit: $limit, offset: $offset) {
items {
name
slug
price {
centAmount
currencyCode
}
}
total
}
}
```
### What gets generated
[Section titled “What gets generated”](#what-gets-generated)
Running `pnpm codegen` produces two outputs:
**`mcp-tools.generated.ts`**, an array of tool definitions:
```typescript
export const generatedTools: GeneratedMCPTool[] = [
{
name: "get_products",
description: "Search for products by query, with pagination",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query for products" },
limit: { type: "integer", description: "Number of results to return" },
offset: { type: "integer", description: "Pagination offset" },
},
},
documentId: "sha256:abc123...",
queryString: "query GetProducts($query: String, ...) { ... }",
},
// ...
];
```
**`persisted-documents.json`**, a mapping of document IDs (SHA-256 hashes) to query strings. These are registered with GraphQL Hive at deploy time and act as an allowlist. The gateway rejects any query not in this registry. See [Query security](/architecture/mcp/architecture/#query-security--persisted-documents) for the full security model.
### Type mapping
[Section titled “Type mapping”](#type-mapping)
The codegen automatically maps GraphQL types to JSON Schema:
| GraphQL type | JSON Schema type |
| ------------- | --------------------------- |
| `String` | `string` |
| `Int` | `integer` |
| `Float` | `number` |
| `Boolean` | `boolean` |
| `ID` | `string` |
| Enums | `string` with `enum` values |
| Input objects | `object` with `properties` |
| Lists | `array` with `items` |
| Non-null | Added to `required` array |
## Configuration
[Section titled “Configuration”](#configuration)
MCP servers are configured through environment variables. The base configuration class provides sensible defaults:
| Variable | Default | Description |
| --------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `HTTP_HOST` | `0.0.0.0` | HTTP server bind address |
| `HTTP_PORT` | `6000` | HTTP server port |
| `MCP_ENDPOINT_PATH` | `/mcp` | MCP endpoint path |
| `GATEWAY_ENDPOINT` | `http://localhost:4000/graphql` | GraphQL gateway endpoint URL |
| `JWT_ISSUER` | `http://localhost:4000` | Expected JWT issuer |
| `JWT_AUDIENCE` | `http://localhost:4000` | Expected JWT audience |
| `PROXY_AUTH_ENABLED` | `false` | Allow a trusted proxy to forward a pre-authenticated session via the `X-Access-Token` / `X-Data-Token` / `X-Refresh-Token` headers (takes precedence over `Authorization: Bearer`, skips JWT pre-validation — the gateway still enforces token validity) |
| `TOKEN_REFRESH_THRESHOLD_SECONDS` | `300` | Refresh tokens this many seconds before expiry |
| `RATE_LIMIT_MAX` | `100` | Max requests per window |
| `RATE_LIMIT_WINDOW_SECONDS` | `60` | Rate limit window duration |
## Built-in plugins
[Section titled “Built-in plugins”](#built-in-plugins)
### Customer auth plugin
[Section titled “Customer auth plugin”](#customer-auth-plugin)
The customer-auth plugin handles login and logout flows by intercepting the `customer_login` and `customer_logout` tools. It manages session transitions between guest and authenticated states, including:
* Executing login/logout mutations against the GraphQL gateway
* Storing new tokens in the session authentication context
* Creating a guest session automatically after logout
* Token refresh before expiry
# mcp-customer
> The mcp-customer service, a customer-facing MCP server exposing product, cart, order, and account tools.
`@evolve-platform/mcp-customer` is a customer-facing MCP server built on `mcp-core`. It exposes the tools that an AI assistant needs to help customers browse products, manage their cart, view orders, and handle their account.
## Available tools
[Section titled “Available tools”](#available-tools)
### Authentication
[Section titled “Authentication”](#authentication)
| Tool | Description |
| ------------------ | ---------------------------------------- |
| `customer_login` | Log in with email and password |
| `customer_logout` | Log out and revert to a guest session |
| `get_current_user` | Get the currently authenticated customer |
### Products
[Section titled “Products”](#products)
| Tool | Description |
| ------------------------- | ---------------------------------------- |
| `get_products` | Search products with filters and sorting |
| `get_product_detail_page` | Get full product details by slug |
| `get_products_by_skus` | Batch retrieve products by SKU |
### Cart
[Section titled “Cart”](#cart)
| Tool | Description |
| --------------------------- | -------------------------- |
| `get_cart` | Get the active cart |
| `cart_line_items_add` | Add products to the cart |
| `cart_line_items_remove` | Remove items from the cart |
| `cart_line_items_update` | Update item quantities |
| `cart_discount_code_add` | Apply a discount code |
| `cart_discount_code_remove` | Remove a discount code |
### Checkout
[Section titled “Checkout”](#checkout)
| Tool | Description |
| --------------------------- | ------------------------------------------------------------------------------- |
| `get_cart_checkout_details` | Get cart details for checkout, including available shipping and payment methods |
| `cart_set_shipping_address` | Set the shipping address on the cart |
| `cart_set_billing_address` | Set the billing address on the cart |
| `cart_set_customer_email` | Set the customer email on the cart |
| `cart_set_shipping_method` | Select a shipping method |
| `cart_set_payment_method` | Select a payment method |
| `checkout_complete` | Complete the checkout and create an order |
| `address_validate` | Validate an address, with correction suggestions |
### Shopping lists
[Section titled “Shopping lists”](#shopping-lists)
| Tool | Description |
| ----------------------------- | ----------------------------------- |
| `get_customer_shopping_lists` | List all shopping lists (paginated) |
| `get_customer_shopping_list` | Get a specific shopping list |
| `add_to_shopping_list` | Add items to a shopping list |
| `remove_shopping_list_item` | Remove items from a shopping list |
Note
Shopping list tools require an authenticated session. The AI client should call `customer_login` first.
### Customer account
[Section titled “Customer account”](#customer-account)
| Tool | Description |
| ------------------------- | --------------------------------------- |
| `get_customer` | Get customer data with addresses |
| `customer_address_create` | Create a new address |
| `customer_address_update` | Update an existing address |
| `customer_address_delete` | Remove an address from the address book |
### Orders
[Section titled “Orders”](#orders)
| Tool | Description |
| --------------------- | ------------------------------------------------------- |
| `order_history_query` | Get the customer’s order history |
| `get_order_detail` | Get detailed information about an order by order number |
## Example interaction
[Section titled “Example interaction”](#example-interaction)
Here is what a typical AI-assisted shopping session looks like from the tool invocation perspective:
```
sequenceDiagram
participant User as Customer
participant AI as AI Assistant
participant MCP as mcp-customer
User->>AI: I'm looking for running shoes
AI->>MCP: get_products(query: "running shoes")
MCP-->>AI: Product results
AI->>User: Here are some options...
User->>AI: Tell me more about the Nike Air Zoom
AI->>MCP: get_product_detail_page(slug: "nike-air-zoom")
MCP-->>AI: Full product details
AI->>User: Details and pricing...
User->>AI: Add it to my cart, size 42
AI->>MCP: cart_line_items_add(sku: "nike-air-zoom-42", quantity: 1)
MCP-->>AI: Updated cart
AI->>User: Added to your cart!
User->>AI: I have a discount code SAVE10
AI->>MCP: cart_discount_code_add(id: "", code: "SAVE10")
MCP-->>AI: Cart with discount applied
AI->>User: Discount applied, your new total is...
```
## Running locally
[Section titled “Running locally”](#running-locally)
```bash
cd backend/services/mcp-customer
# Generate tools from GraphQL operations
pnpm codegen
# Start with HTTP transport (port 4050)
pnpm dev
# Start with stdio transport (for Claude Desktop)
pnpm dev:stdio
```
Caution
The GraphQL gateway must be running for the MCP server to function. Start it with `pnpm dev:backend` from the repository root.
## Store context
[Section titled “Store context”](#store-context)
The mcp-customer service supports multi-store configurations. When connecting over HTTP, clients can pass store context via headers:
| Header | Description |
| ------------------------------ | -------------------------------------- |
| `X-StoreContext-Locale` | Locale for the session (e.g., `en-US`) |
| `X-StoreContext-Currency` | Currency code (e.g., `EUR`) |
| `X-StoreContext-StoreKey` | Store identifier |
| `X-StoreContext-CustomerGroup` | Customer group for pricing |
# Why MCP on top of GraphQL Federation?
> Why Evolve builds MCP on top of GraphQL Federation instead of connecting AI directly to vendor APIs.
Many SAAS vendors (commerce platforms, CMS providers, payment services) are starting to ship their own MCP servers. While this may seem convenient, it introduces the same problem that GraphQL Federation solves for traditional channels: you lose the unified data model and business logic that you’ve built on top of these services.
## Where vendor MCP servers make sense
[Section titled “Where vendor MCP servers make sense”](#where-vendor-mcp-servers-make-sense)
Vendor MCP servers are not without merit. They can be genuinely useful for administrative and operational tasks that happen outside the customer experience: managing product catalogues, configuring promotions, updating content, or querying analytics dashboards. These are tasks where you interact with a single vendor’s back-office and don’t need cross-service consistency or your own business rules applied on top.
The distinction matters: when an AI assistant helps a merchandiser update a product description in Storyblok, going directly to Storyblok’s API is fine. But when an AI assistant helps a customer find and buy that product, it needs to go through your platform to get the right price, availability, and context.
## The problem with vendor MCP servers for customer experiences
[Section titled “The problem with vendor MCP servers for customer experiences”](#the-problem-with-vendor-mcp-servers-for-customer-experiences)
When an AI assistant connects directly to a vendor’s MCP server for customer-facing use cases, it bypasses everything your platform has built:
* **Custom data models**: the domain-specific schemas you’ve designed through your subgraphs are ignored. The AI sees raw vendor data instead.
* **Cross-service orchestration**: business flows that span multiple services (e.g., checking inventory in one system and pricing in another) can’t work when each vendor is accessed in isolation.
* **Pricing and promotions**: overrides, customer-group pricing, discount logic, and currency handling that live in your domain services are skipped.
* **Localisation**: language, locale, and regional rules applied at the federation layer don’t exist at the vendor API level.
* **Access control**: authentication and authorization policies enforced by your gateway are bypassed entirely.
The result is an AI experience that is inconsistent with your storefront, mobile app, and any other channel, even though they all sell the same products to the same customers.
## The Evolve approach
[Section titled “The Evolve approach”](#the-evolve-approach)
By building MCP on top of Evolve’s GraphQL Federation layer, AI experiences are **completely consistent with every other channel**. Your storefront, mobile app, and AI assistant all go through the same gateway, the same domain services, and the same business rules.
**Evolve approach**
```
graph TD;
ai["AI Assistant"]-->mcp["MCP Server"]
mcp-->gw["GraphQL Gateway"]
web["Storefront"]-->gw
gw-->svc["Domain Services"]
```
**Vendor MCP approach**
```
graph TD;
ai["AI Assistant"]-->vendor-mcp["Vendor MCP"]-->saas["SAAS API"]
web["Storefront"]-->gw["GraphQL Gateway"]-->svc["Domain Services"]-->saas
```
Everything your team has implemented in the subgraphs (pricing logic, inventory rules, cart validation, promotions) is automatically available to AI clients without duplication. Adding an AI channel is not a separate integration, it’s another client of the same architecture.
## In practice
[Section titled “In practice”](#in-practice)
This means that when you define an MCP tool like `get_products`, the AI gets exactly the same product data, with the same prices, availability, and localisation, as a customer browsing your storefront. If you change a pricing rule or add a new promotion, the AI channel picks it up immediately because it goes through the same code path.
The same applies in reverse: any new subgraph or integration you add to Evolve can be exposed to AI clients by simply writing a GraphQL operation and annotating it with `@mcpTool`. No separate vendor integration required.
# Modernizing an existing stack
> How Evolve runs on top of an existing commerce engine to give you control of the stack, a modern experience layer, and an optional path to change engines.
You do not have to replace your commerce engine to modernize. Evolve can run on top of an engine you already operate: the engine keeps running as one of the sources behind the [GraphQL Federation](/architecture/core/graphql/) layer, while Evolve adds the experience and orchestration layer around it.
## You control the stack
[Section titled “You control the stack”](#you-control-the-stack)
With Evolve in place, the parts that shape your commerce experience move into your own codebase instead of the engine’s proprietary customization model:
* **Data model and business logic:** the [domain services](/architecture/core/design/) define Evolve’s [own data model](/architecture/core/datamodel/) and the logic on top of it. Each service maps the engine’s data into that model.
* **API contract:** the [GraphQL Federation](/architecture/core/graphql/) supergraph is the single integration point. Storefronts, apps, and agents depend on your schema, not on a vendor API.
* **The engine as an adapter:** the commerce engine is integrated as one [commerce-engine adapter](/architecture/core/commerce-engines/) among your SAAS backends, alongside CMS, search, and payment providers.
The result is [vendor independence](/architecture/principles/#vendor-independence) in practice: you decide what each part of the stack runs on, and you can change it without rebuilding your channels.
## What you gain on top of any engine
[Section titled “What you gain on top of any engine”](#what-you-gain-on-top-of-any-engine)
Because these capabilities live in the Evolve layer, they work regardless of the commerce engine underneath. Adopting Evolve on an existing engine makes them available without changing the backend.
**Experience**
* A modern [Next.js storefront](/architecture/frontend/) across web and mobile.
* Pluggable [CMS integration](/architecture/core/design/#cms) with Contentful or Storyblok, including live editing, which many engine deployments lack out of the box.
* The [AI authoring layer](/evolve/authoring/) for generating and editing pages in conversation with an AI on top of the live storefront.
**Agentic**
* [AI agent integration through MCP](/architecture/mcp/), which exposes your commerce capabilities as tools that AI assistants can discover and invoke, with the same business rules as the storefront.
**Operations**
* Built-in [observability](/architecture/cloud/observability/) through OpenTelemetry, with distributed tracing and metrics across services.
* [Cloud-native deployment](/architecture/cloud/) on AWS, GCP, or Azure, with Vercel hosting on the roadmap, and infrastructure managed as code.
Search is also pluggable, though the search integration depends on the engine adapter. See [search](/architecture/infrastructure/search/) for what each engine supports.
## Changing engines over time
[Section titled “Changing engines over time”](#changing-engines-over-time)
Because every [commerce-engine adapter](/architecture/core/commerce-engines/) implements the same federated schema, the engine is not a permanent choice. A domain can resolve from a different engine without changing the schema that consumers depend on.
That makes a gradual, reversible transition from one engine to another possible. Two approaches fit the federation model:
* **Per domain:** move one domain at a time to a new engine, for example catalog first and checkout later, while the rest keeps resolving from the current engine.
* **Per market or brand:** route different markets or brands to different engines through the same supergraph, based on [store context](/architecture/core/store-context/).
This is an architectural capability that GraphQL Federation enables rather than a turnkey feature: a concrete transition is designed per project. It does mean the decision to change engines is no longer a single, all-or-nothing replatform.
# Payment architecture
> How payment providers integrate with the checkout service and commercetools.
The checkout service orchestrates payment processing by coordinating between payment provider services (Stripe, Adyen, Mollie, PayNL, etc.) and commercetools. Each payment provider is a standalone service that handles communication with its PSP. The provider only updates the Payment object and its transactions. Order-level actions (confirming an order, handling cancellations) are handled by the checkout service through [event subscriptions](/architecture/infrastructure/messaging/).
## Sequence diagram
[Section titled “Sequence diagram”](#sequence-diagram)
```
sequenceDiagram
actor CU as Customer
participant WE as Website
participant CO as Checkout
participant PS as PSP service
participant CT as commercetools
participant PSP as PSP
participant BANK as Bank
Note over CU,BANK: Start payment flow
CU->>WE: Start payment step
activate WE
WE ->> CO: createPayment
activate CO
CO ->> CT: get order
activate CT
CT -->> CO: order data
CO ->> CT: create Payment on order
activate CT
CO ->> PS: create transaction
activate PS
PS ->> PSP: create transaction
activate PSP
PSP -->> PS: transaction object
deactivate PSP
PS ->> CT: create Transaction on payment
deactivate CT
deactivate CT
PS -->> CO: transaction result
deactivate PS
CO -->> WE: return payment redirect URL
deactivate CO
WE-->>CU: Redirect customer to PSP
activate PSP
CU->>PSP: Pay order at PSP
PSP->>BANK: redirect to bank
activate BANK
BANK ->> BANK: pay/cancel/else
BANK-->>PSP: return payment result
deactivate BANK
PSP-->>WE: Return to returnURL
deactivate PSP
WE->>CO: Update order payment status
activate CO
CO->>CT: Fetch order by transaction ID
activate CT
CT-->>CO: Return order
deactivate CT
CO->>PS: Fetch transaction status for order
activate PS
PS->>PSP: Fetch transaction status
activate PSP
PSP-->>PS: Return transaction status
deactivate PSP
PS->>CT: Update transaction status
activate CT
PS-->>CO: Return transaction status
deactivate PS
deactivate CT
CO->>CT: Fetch update order
activate CT
CT-->>CO: Return updated order
deactivate CT
CO-->>WE: Return order with payment status
deactivate CO
WE->>CU: Show confirmation page
deactivate WE
```
## Creating a payment
[Section titled “Creating a payment”](#creating-a-payment)
When a payment is needed, the frontend calls the `createPayment` mutation. The checkout service then:
1. Fetches the order from commercetools
2. Creates a Payment object on the order with the selected method and payment interface
3. Delegates to the appropriate PSP service to start a transaction
4. Returns a redirect URL so the customer can complete payment
The Payment object stores the selected method in `paymentMethodInfo.method` and the provider identifier in `paymentMethodInfo.paymentInterface`.
## Payment states
[Section titled “Payment states”](#payment-states)
The checkout service manages payment state transitions through commercetools state machines. Payment providers transition the Payment to one of these states:
| State | Key | Environment variable override |
| --------- | ------------------ | ----------------------------- |
| Initial | `PaymentInitial` | `PAYMENT_STATE_KEY_INITIAL` |
| Pending | `PaymentPending` | `PAYMENT_STATE_KEY_PENDING` |
| Success | `PaymentSuccess` | `PAYMENT_STATE_KEY_SUCCESS` |
| Cancelled | `PaymentCancelled` | `PAYMENT_STATE_KEY_CANCELLED` |
| Failure | `PaymentFailure` | `PAYMENT_STATE_KEY_FAILURE` |
These state objects are defined in the checkout service’s Terraform configuration and looked up by key at runtime. You can override the default keys through environment variables if your project uses different naming.
### Event-driven order updates
[Section titled “Event-driven order updates”](#event-driven-order-updates)
When a payment state changes, commercetools emits a `PaymentStatusStateTransition` message. The checkout service subscribes to these messages and updates the order accordingly. For example, when all payments on an order reach `PaymentSuccess`, the checkout service transitions the order’s payment state to paid.
This event-driven approach means the checkout service reacts to payment outcomes asynchronously rather than polling for status changes. See [Messaging & events](/architecture/infrastructure/messaging/) for how event subscriptions work across all services.
## REST endpoints
[Section titled “REST endpoints”](#rest-endpoints)
A payment provider exposes the following API endpoints:
**POST /payment-methods**: return the payment methods available for the given cart. The cart ID and other context are passed in the request body.
**POST /create**: create a payment transaction. This starts the payment flow and returns a redirect URL for the customer to complete payment at the PSP.
**POST /push** (webhook): receive asynchronous payment status updates from the PSP. This endpoint handles webhook callbacks and updates the transaction status in commercetools. See [REST endpoints & webhooks](/architecture/infrastructure/rest-webhooks/) for how each provider verifies webhook authenticity.
A payment provider may also expose:
**GET /redirect**: handle customer redirects back from the PSP after completing payment.
**POST /validate-apple-pay-session**: validate an Apple Pay merchant session for Apple Pay payments.
## API contract
[Section titled “API contract”](#api-contract)
The payment API contract is defined as TypeBox schemas in the `@evolve-framework/commercetools` package (`packages/commercetools/src/payments/schema.ts`). The two primary endpoints and their types are documented below.
### POST /create
[Section titled “POST /create”](#post-create)
Starts a payment transaction and returns a redirect URL for the customer to complete payment at the PSP.
**Request body: `TransactionRequest`**
| Field | Type | Required | Description |
| ------------------- | ------------- | -------- | -------------------------------------- |
| `amount` | `Price` | yes | Transaction amount |
| `customer` | object | yes | `{ ipAddress: string }` - IPv4 or IPv6 |
| `locale` | string | yes | e.g. `en-GB`, `nl-NL` |
| `targetId` | string (UUID) | yes | commercetools cart or order ID |
| `targetType` | string | yes | `cart` or `order` |
| `paymentId` | string (UUID) | yes | commercetools payment ID |
| `paymentMethod` | string | yes | Payment method code |
| `paymentMethodArgs` | object | yes | Provider-specific arguments |
| `successUrl` | string (URL) | yes | Redirect after successful payment |
| `failureUrl` | string (URL) | yes | Redirect after failed payment |
**Response body: `TransactionResponse`**
| Field | Type | Description |
| --------------------- | ------------- | ---------------------- |
| `status` | string | Transaction status |
| `payload.id` | string (UUID) | Transaction identifier |
| `payload.redirectURL` | string (URL) | Customer redirect URL |
### POST /payment-methods
[Section titled “POST /payment-methods”](#post-payment-methods)
Returns the payment methods available for the given context (cart, locale, country).
**Request body: `PaymentMethodArgs`**
| Field | Type | Required | Description |
| ------------ | ------------- | -------- | ---------------------------------- |
| `targetId` | string (UUID) | no | Cart or order to check methods for |
| `targetType` | string | yes | `cart` or `order` |
| `currency` | string | yes | e.g. `EUR`, `USD` |
| `country` | string | yes | e.g. `NL`, `DE` |
| `locale` | string | yes | e.g. `en-GB`, `nl-NL` |
| `device` | string | yes | `web`, `ios`, or `android` |
**Response body: `PaymentMethod[]`**
| Field | Type | Description |
| --------------- | ------ | --------------------------------------------------------- |
| `id` | string | Method identifier |
| `localizedName` | string | Display name for the requested locale |
| `provider` | string | PSP provider key |
| `issuers` | array | Sub-options (e.g. iDEAL banks), each with `id` and `name` |
### Shared types
[Section titled “Shared types”](#shared-types)
**Price**
| Field | Type | Description |
| ------------ | ----------------------- | ------------------------------------ |
| `currency` | string | ISO 4217 currency code |
| `centAmount` | number (integer format) | Amount in the smallest currency unit |
# Design principles
> The design principles behind Evolve's architecture and how they shape the platform.
These principles guide how Evolve is built and how it is intended to be used by project teams.
## Vendor independence
[Section titled “Vendor independence”](#vendor-independence)
Evolve owns its [data model](/architecture/core/datamodel/) and does not expose vendor APIs directly. Each domain service maps vendor-specific data into Evolve’s GraphQL schema, which means you can swap or combine SAAS backends without changing the API your frontend consumes. This applies across the board: commerce engines, CMS platforms, search engines, payment providers, and PIM systems.
## Composability
[Section titled “Composability”](#composability)
The platform is modular by design. You pick the [domain services](/architecture/core/design/) you need, combine them through [GraphQL Federation](/architecture/core/graphql/), and add or replace services over time. Need a different CMS? Replace one subgraph. Need B2B? Add the business unit and quote services. The architecture supports any composition without requiring changes to unrelated services.
## Project autonomy
[Section titled “Project autonomy”](#project-autonomy)
Evolve is a starting point, not a constraint. Projects built on Evolve should be able to evolve freely without being limited by the platform’s defaults. The [framework](/architecture/core/framework/) provides shared logic that projects can use as-is, override selectively, or extend with custom modules. Your project owns its own services, configuration, and deployment, and can diverge from the defaults wherever your use case requires it.
## Developer experience
[Section titled “Developer experience”](#developer-experience)
The codebase is worked on by developers across different companies and experience levels. This means code should be straightforward to read, understand, and modify. Evolve favours explicit patterns over clever abstractions: services follow a consistent [service structure](/architecture/core/framework/architecture/#anatomy-of-a-service), GraphQL modules have a predictable shape, and shared logic lives in well-documented packages rather than hidden behind framework magic.
## Cloud-native
[Section titled “Cloud-native”](#cloud-native)
Services are designed as [twelve-factor apps](https://12factor.net/): stateless, configured through environment variables, with structured logging and graceful shutdown. This makes them portable across [cloud providers](/architecture/cloud/) (AWS, GCP, Azure) and straightforward to scale, monitor, and operate in production.
## Multi-channel
[Section titled “Multi-channel”](#multi-channel)
The GraphQL Federation layer serves all channels through a single API: web storefronts, mobile apps, and [AI agents](/architecture/mcp/) through the Model Context Protocol. Business logic is implemented once in the domain services and shared across all channels, rather than duplicated per client.
## Infrastructure as code
[Section titled “Infrastructure as code”](#infrastructure-as-code)
Everything is defined declaratively in Terraform: cloud resources, [SAAS service configuration](/architecture/cloud/saas-configuration/) (commercetools projects, CMS content models, search indices), and deployment pipelines. A new environment, including all its dependencies, can be provisioned from a single command with no manual setup.
# Background articles
> Recommended background articles to read.
We’ve written several in-depth background articles about how we approach composable architecture, and which effectively describe how Evolve is built.
The articles cover areas such as Performance, Security, Developer Experience and infrastructure-as-code.
* [How we make MACH platforms highly performant](https://blog.labdigital.nl/how-we-make-mach-platforms-highly-performant-3d81ad3e4955)
* [How to make MACH platforms highly secure](https://blog.labdigital.nl/how-to-make-mach-platforms-secure-59f4b5e8a954)
* [Using monorepos for MACH projects](https://blog.labdigital.nl/end-to-end-monorepos-for-mach-4a2af8a501f5)
* [Development platforms for MACH](https://blog.labdigital.nl/why-we-should-not-only-be-talking-about-accelerators-for-mach-22277b08f873)
* [Terraform for MACH services](https://blog.labdigital.nl/how-a-terraform-provider-is-valuable-for-any-mach-service-vendor-d491d6625bab)
* [GraphQL Federation & Composable Architecture: a Cloud Native love story](https://blog.labdigital.nl/graphql-federation-composable-architecture-a-cloud-native-love-story-d30fc46713fb)
* [An overview of our open source projects for MACH & Composable](https://blog.labdigital.nl/an-overview-of-our-open-source-projects-for-mach-composable-abda5f889382)
* [Reaching MACHs full potential with MACH composer](https://blog.labdigital.nl/reaching-machs-full-potential-b336507f2f99)
* [Introducing MACH composer](https://blog.labdigital.nl/introducing-mach-composer-977abbd10664)
# Changelog
We record notable changes to Evolve in this file.
[2025.10.0 (2025-10-31)](/changelog/2025.10.0/)
[2025.11.0 (2025-11-26)](/changelog/2025.11.0/)
[2025.12.0 (2025-12-24)](/changelog/2025.12.0/)
[2025.2.0 (2025-02-28)](/changelog/2025.2.0/)
[2025.3.0 (2025-03-31)](/changelog/2025.3.0/)
[2025.4.0 (2025-04-25)](/changelog/2025.4.0/)
[2025.5.0 (2025-05-28)](/changelog/2025.5.0/)
[2025.6.0 (2025-06-30)](/changelog/2025.6.0/)
[2025.7.0 (2025-07-31)](/changelog/2025.7.0/)
[2025.8.0 (2025-08-27)](/changelog/2025.8.0/)
[2025.9.0 (2025-09-30)](/changelog/2025.9.0/)
[2026.1.0 (2026-01-30)](/changelog/2026.1.0/)
[2026.2.0 (2026-03-02)](/changelog/2026.2.0/)
[2026.3.0 (2026-04-02)](/changelog/2026.3.0/)
[2026.4.0 (2026-04-30)](/changelog/2026.4.0/)
[2026.5.0 (2026-05-26)](/changelog/2026.5.0/)
[2026.6.0 (2026-06-29)](/changelog/2026.6.0/)
[2026.7.0 (2026-07-31)](/changelog/2026.7.0/)
[2026.8.0 (2026-08-27)](/changelog/2026.8.0/)
# 2025.10.0 (2025-10-31)
### Storefront
[Section titled “Storefront”](#storefront)
#### Feature
[Section titled “Feature”](#feature)
* Add proper thank you page after order
* Add catalog block component
* Add order number to success URL
#### Changed
[Section titled “Changed”](#changed)
* Rename quote-summary component to summary
* Add menu icons to my account navigation
* Fix product grid layout
* Allow for not passing first and last name
### Backend
[Section titled “Backend”](#backend)
#### Feature
[Section titled “Feature”](#feature-1)
* Added better implementation of CategoryBlock and CategoryBlockItem
* Upgraded redis client version, added entraid usage for azure redis client
* Implement product sync done webhook handling for bluestone pim
#### Changed
[Section titled “Changed”](#changed-1)
* Changed orderConfirmation query to orderById to hide order number
* Fetch correct store distribution and supply channels
* Fixed email notification client loading. Updated configs
* Made validation check on content snippet smarter
* Renamed email component to email-smtp
### Platform
[Section titled “Platform”](#platform)
#### Feature
[Section titled “Feature”](#feature-2)
* Added logger adapter for ServerlessAdapter lib
#### Changed
[Section titled “Changed”](#changed-2)
* Fixed email service startup
* Removed all bun package.json scripts
# 2025.11.0 (2025-11-26)
### Storefront
[Section titled “Storefront”](#storefront)
#### Feature
[Section titled “Feature”](#feature)
* Add content to account overview page
* Add composable address cards
#### Changed
[Section titled “Changed”](#changed)
* Fixed possibility of getting into infinite refresh loop
* Style order cards conform to the design
* Align accordion component with shadcn/ui
* Merge quantity selectors into a single component
* Resolved minor styling issues
### Backend
[Section titled “Backend”](#backend)
#### Feature
[Section titled “Feature”](#feature-1)
* Set composite token to expire in 1 day
* Added messageGroupId usage for azure servicebus and eventgrid publisher
* Added internal events emitters for products
* Added catalog sitemap generator
#### Changed
[Section titled “Changed”](#changed-1)
* Set fetching of private domain fn to same as public
* Updated expiration times of composite cookie to match underlying tokens
* Fixed build step for product-sync job in catalog service
* Improved Azure Service Bus error handling
### Platform
[Section titled “Platform”](#platform)
#### Dependency
[Section titled “Dependency”](#dependency)
* Update to vitest 4.x, see
# 2025.12.0 (2025-12-24)
### Storefront
[Section titled “Storefront”](#storefront)
#### Feature
[Section titled “Feature”](#feature)
* Fixed sitemap generation, added static sitemap example code
#### Dependency
[Section titled “Dependency”](#dependency)
* Update to Storybook 10.x
### Backend
[Section titled “Backend”](#backend)
#### Feature
[Section titled “Feature”](#feature-1)
* Expose generated product sitemaps
* Added missing resolver code for cms-contentful pages
#### Changed
[Section titled “Changed”](#changed)
* Added additional options to storage client, implemented in Azure Blob storage client
* Added process.exits to all the catalog-commercetools jobs
* Made commercetools client more resilient
### Platform
[Section titled “Platform”](#platform)
#### Feature
[Section titled “Feature”](#feature-2)
* Updated to nextjs 15.x
# 2025.2.0 (2025-02-28)
For the February 2025 release the main focus was getting the frontend architecture in a better state. We moved to Tailwind v4, cleaned up our GraphQL fragments, unified our previously scattered React Hook Form implementations, and brought consistency to our component structure. Deployments are now also more robust by syncing static files to cloud storage before running the new version, removing javascript errors mid-deploy for end-users.
### Added
[Section titled “Added”](#added)
#### Platform
[Section titled “Platform”](#platform)
* Move to trusted documents for GraphQL queries, improving both performance and security
* GraphQL schema viewer in documentation for better API exploration
#### Frontend
[Section titled “Frontend”](#frontend)
* Upload Nextjs static files to cloud storage and serve them from the loadbalancer. This makes deployments more stable as both the old and new version of all static files are always available.
* Added Storybook test addon for component verification
* Support for testing UI components outside of Storybook
* Component tests for UI elements including alert-dialog
* New generic error dialog system with improved dialog animations and button interactions
* Improved account pages and B2C order overview experience with better dashboard, breadcrumbs and order history
* Next-intl storybook plugin for better internationalization testing
* Messages watch command for site development
* Simple PDP interaction test with mock response for component validation
#### Backend
[Section titled “Backend”](#backend)
* Improve error responses for Cart mutations by returning a union of `Cart | GenericError`
* Automatically set billing and shipping address on new carts for authenticated users based on the defaults
* Load supergraph from Hive when environment variables are available
* JWT token validation for secure authentication
* API-extensions bearer token validation for Commercetools
### Changed
[Section titled “Changed”](#changed)
#### Platform
[Section titled “Platform”](#platform-1)
* Improve code quality of terrafom modules and use tflint for validation
* Upgrade from Sentry 7 to 9
* Improve Sentry sourcemap uploading (use debug ids)
#### Frontend
[Section titled “Frontend”](#frontend-1)
* **Generic**
* Health check extension with GraphQL query to verify gateway connectivity
* Extended storybook test coverage for content pages
* TypeScript implementation for icons script
* Updated frontend coding guidelines
* Enhanced frontend components for shadcn compatibility
* Removed unused components (dropdown, check) and standardized remaining ones
* Moved validation schemas closer to their usage points
* Ensured consistent filename patterns in frontend
* **GraphQL Fragment Management**
* Removed generated fragment usage from components and stories
* Using ResultOf instead of directly importing fragments
* Added suffix to all fragments for better identification
* Colocated fragments with their components (PDP)
* **Component Reorganization**
* Moved product header and specifications to UI package
* Improved PDP according to new architecture with colocated fragments
* Relocated add-to-cart dialogs to main folder components
* Cleaned up UI package structure and standardized components
* Replaced modal and old dialog with new reusable dialog component
* **Form Handling**
* Standardized React Hook Form usage across the application (previously had multiple inconsistent implementations)
* Consolidated form handling patterns in checkout and other areas
* Removed custom useForm hook and deprecated form implementations
* Integrated zod error map into form resolver
* Moved form schemas closer to their usage
* Simplified validation patterns and improved error handling
* **Data Flow Improvements**
* Consolidated page data dependencies to a single file (removed \_data folder)
* Renamed gqlServerFetch to serverClient for consistency
* Added field colocating for content pages
* Standardized naming conventions for data fetching operations (get/post instead of fetch)
#### Backend
[Section titled “Backend”](#backend-1)
* Simplified custom object dataloader
* Relocated price resolver to separate file for better customization
* Refactored variant dimensions to use interfaces instead of unions for better API usability
* Improved Algolia sync for prices (support for standalone prices)
* Allow bypassing trusted documents check using HTTP Header with secret token
* Improved concurrency handling in bluestone pim component
#### Dependencies
[Section titled “Dependencies”](#dependencies)
* Upgraded to Tailwind v4 with automated migration
* Updated Vitest to 3.x
* Updated PNPM to 10.x (with explicit post-install script configuration)
* Updated @labdigital/graphql-fetcher to 2.0
* Updated Sentry to 9.x
* Updated Algolia to 5.x
### Security
[Section titled “Security”](#security)
* Added Content Security Policy (CSP) headers to site
* Improved configuration for static file security
* Enhanced JWT token validation with proper segment checking
* Fixed bearer token validation for API-extensions
# 2025.3.0 (2025-03-31)
In this sprint, our primary focus was on enhancing both the frontend and backend of our application. On the frontend, we introduced new features such as recently viewed products and improved the user experience with a new hero component and better mobile support. We also made significant refactoring efforts, including the cart page and various components for better accessibility and performance. On the backend, we added support for Azure Service Bus and a new monitoring service, while also updating contentful models and improving GraphQL queries. Additionally, we moved our documentation to a separate repository to streamline our development process
### Feature
[Section titled “Feature”](#feature)
#### Platform
[Section titled “Platform”](#platform)
* Added automatic changelog generation tools to better track changes
#### Frontend
[Section titled “Frontend”](#frontend)
* Added recently viewed products
* Use content source maps for live preview in contentful
#### Backend
[Section titled “Backend”](#backend)
* Added support for messaging with azure service bus
* Added monitoring service to keep track of placed orders
### Changed
[Section titled “Changed”](#changed)
#### Platform
[Section titled “Platform”](#platform-1)
* Moved documentation to separate repository to streamline development process
#### Frontend
[Section titled “Frontend”](#frontend-1)
* Refactor cart page and add storybook for it
* Refactor cart page query to suspense query
* Replaced next-seo with manual json-ld declarations
* Replaced font with Archivo using next/font/google
* Removed custom auth mutation hooks from login/register
* Refactored footer with separate footer query
* Added new hero component with better mobile support
* Restyled product card and teaser row
* Improved a11y in top bar and price components
* Add missing id field to AddressInput in GraphQL
* Enabled vercel toolbar in frontend
* Restyling product specifications and added tooltip
#### Backend
[Section titled “Backend”](#backend-1)
* Updated Contentful models to be in line with the CMS and frontend
### Dependency
[Section titled “Dependency”](#dependency)
* Update to TypeScript 5.8 and enable `erasableSyntaxOnly` for backend services
* Updated mach-composer to version 2.22.0
* Updated next to 15.2
* Updated terraform provider
* Updated federated token package to 1.6.0
# 2025.4.0 (2025-04-25)
### Feature
[Section titled “Feature”](#feature)
* Added emitting of internal catalogue category events
* Added tokencredentials support to redis client for Azure
* Pin all GitHub workflows using pinact
* Add a generic error handler which inspects the error returned by commercetools and returns a generic error message to the user.
* Separate footer content type in Contentful
* Add Sentry integration for Fastify
* Introduce Knip in the pipelines and clean up code to pass all checks
* Introduce new CatalogPage type and automatically create these based on category created events from the catalog services in the CMS
* Add e2e test up to checkout including improved page fixtures
* Add mega menu to the site
### Changed
[Section titled “Changed”](#changed)
* Added fix to also run deployment when only configuration TF has changed
* Implement site-header-content from contentful
* Replace hardcoded strings with translatable messages in site
* Fixed azure honeycomb configuration
* Use the `createSupergraphManager` from the latest `@graphql-hive/apollo` package instead of our own implementation. This is possible due to upstream fix
* Simplified github workflows
* Added all locales to contentful configuration
* Refactor the payment services to use fastify instead of express
* Use fastify to run the Apollo Gateway instead of express
* Added redis error logging
* Minor refactor to payment services to use JSON Schema’s for request/response validation
* Fetch available contentful locales from cache, allow for undefined fields
* Header refactor and redesign
* Switch checkout-commercetools to use service bus for commercetools events
* Use service bus to feed email component
* Added azure support to feed-products
* Moved algolia to separate configuration folder
* Move all generated files to `src/generated/*` instead of using the `src/*.generated.ts` pattern
* Resolve product detail page breadcrumbs based on the content catalog pages
* Moved customer specific site hooks to `src/hooks`
* Remove opentelemtry in client-side code for now as it doesn’t work as it should currently
* Site: Update hookform integration for new react-hook-form version
### Dependency
[Section titled “Dependency”](#dependency)
* Removed unused dependencies in frontend
# 2025.5.0 (2025-05-28)
### Storefront
[Section titled “Storefront”](#storefront)
#### Feature
[Section titled “Feature”](#feature)
* Single page checkout (to replace multipage checkout)
* Added shopping lists for b2b and b2c that allow you to save your products to one or multiple lists (often called wishlists in b2c)
* Add improved filters and faceting for the product listing and search pages
* Added generic address cards
* Add retry payment page when a payment fails in the front-end or during payment flows
#### Changed
[Section titled “Changed”](#changed)
* Use new query `productsBySKU` for retrieving recently viewed products
* Disable linting and typechecking during NextJS build since this is done earlier in the pipeline (making the build faster)
* Made product availability on product-card dynamic
* Replace `getStoreConfigByKey` with `getStoreConfig` to centralise logic
* Rename `NEXT_PUBLIC_GTM_ID` to `GTM_ID` to make the env var run-time configurable
* Move the `storeConfig` to component level instead of as global variable
### Backend
[Section titled “Backend”](#backend)
#### Feature
[Section titled “Feature”](#feature-1)
* Switch from ioredis to node-redis, as the latter is the recommend package and ioredis is in maintenance mode
* Storyblok navigation resolver and content types
* Add category sync for aws storyblok
* Shopping list/wishlist back-end
* Added adyen payment provider to azure environment
* Added logic to cart create api extension to set business unit default addresses
* Added Redis to all azure components
* Add available payment methods to a customers order
#### Changed
[Section titled “Changed”](#changed-1)
* Use new `DataLoaderCache` for the data loaders to simplify the code
* Optimise the recently viewed products by adding a new query `productsBySKU` which returns cached results
* Fixed algolia starting price sorting
* Use sku for product rows instead of product id
* Check for error response in create session call before trying to load tokens
* Use the official storyblok-cli to generate typescript typedefs for Storyblok
### Platform
[Section titled “Platform”](#platform)
#### Feature
[Section titled “Feature”](#feature-2)
* Use `syncpack` to format and lint the package.json files
* Update to NodeJS 24.x
* Implement NextJS cache invalidation based on Contentful webhooks (Azure)
* Added crowdin configurations
* Check the user token expire time and refresh before it expires and set expire time to 7 days
* Automatically logout users of which the refresh token is no longer valid
* Added Azure production release
* Use Azure resource prefixes to differentiate between environments
#### Changed
[Section titled “Changed”](#changed-2)
* Connected AWS internal eventbus and cleaned up configurations
# 2025.6.0 (2025-06-30)
### Storefront
[Section titled “Storefront”](#storefront)
#### Feature
[Section titled “Feature”](#feature)
* Add environment variables provider to share server side env variables with client components
* Payment provider for the checkout that handles PSP specific components and shares it’s state with our checkout
* Add facet filtering and sorting to PLP for mobile visitors
* Added Adyen creditcard support
* Add stripe creditcard support
#### Changed
[Section titled “Changed”](#changed)
* Update to Storybook 9.x
* Don’t use `useCustomer()` hook for business unit checks as this information is stored in the JWT
* Show summary for chosen delivery method
* Made some hardcoded fields translatable
* Improved product listing loading state handling
### Backend
[Section titled “Backend”](#backend)
#### Feature
[Section titled “Feature”](#feature-1)
* Added basic Stripe payment provider support
* Added Stripe webhook handler for succeeded and failed payment intents
* Improve the monolith runner to use a thread per service for better performance
#### Changed
[Section titled “Changed”](#changed-1)
* Use new storyblok-cli (4.x) to generate typescript types
* Improve cache initialization in services to have a single cache handler
* Create a new initialization function called `initEnvironment()` which prepares the service context
* Add `ioredis` compatible client interface for usage with `useRateLimiter` resolving an issue that customerLogin would hang when redis was enabled
### Platform
[Section titled “Platform”](#platform)
#### Feature
[Section titled “Feature”](#feature-2)
* Allow for setting azure service scaling parameters in mach-config
* Update to biome 2.0
#### Changed
[Section titled “Changed”](#changed-2)
* Added `ADYEN_CLIENT_KEY` environment variable to site deployments
* Replace the `ProductInfo` on cart, order and quote line items by the `ProductVariant`
#### Security
[Section titled “Security”](#security)
* Removed view\_customer setting on commercetools\_user\_token
# 2025.7.0 (2025-07-31)
### Storefront
[Section titled “Storefront”](#storefront)
#### Feature
[Section titled “Feature”](#feature)
* Compact hero for PLP
#### Changed
[Section titled “Changed”](#changed)
* Fix language switcher for catalog pages
* RenderBlock fragment refactor
* Serverside queries for (requested-)quotes
* Move all react cms components to `src/components` as these are used within multiple pages
* Added new content types and updated content structure to use blocks
* Style product card conform with design
* Style USPs conform with design
### Backend
[Section titled “Backend”](#backend)
#### Feature
[Section titled “Feature”](#feature-1)
* Added full implementation of new content structure to contentful
* Support range facet when using commercetools search. This uses the new range stats recently reintroduced in commercetools
#### Changed
[Section titled “Changed”](#changed-1)
* Use store locale when order locale is not set when handling payment
* Added correct client ip address to payment providers
* Fixed startup of bluestone pim service in GCP
* Removed unused ProductListingPage type
* Replaced console logs with logger
* Updated storyblock cms component to return same as contentful
* Fixed redis handler check to allow rediss\://
* Improved stripe component configuration
* Added wrapper function to cache handler class
### Platform
[Section titled “Platform”](#platform)
#### Feature
[Section titled “Feature”](#feature-2)
* Adds graphql schema sync check in ci-cd
* Use traefik gateway instead of azure api management
* Added resourcelink type everywhere required
#### Changed
[Section titled “Changed”](#changed-2)
* Removed deprecated graphql fields and related code
* Cleaned up GraphQL hints and unused types
* Improve azure container app scaling and default settings
* Updated azure terraform code to be more generic, removed hardcoded values
* Removed unused variables and code
# 2025.8.0 (2025-08-27)
### Storefront
[Section titled “Storefront”](#storefront)
#### Feature
[Section titled “Feature”](#feature)
* Added SKU to product specifications table
* Render localized variant dimensions in cart line item
* Add format option to USP list and update cart overview component
* Added sticky product information section on PDP
* Added highlighted product attributes to PDP
#### Changed
[Section titled “Changed”](#changed)
* Style product variant selectors conform with design
* Fix all links in MegaMenu using ResourceLink
* Properly configure the NextJS image loader for contentful assets
* Removed newsletter subscription form
* Changed Footer.paymentMethods to use ResourceLink
### Backend
[Section titled “Backend”](#backend)
#### Feature
[Section titled “Feature”](#feature-1)
* Added hive plugin for apollo smarter TTL sampler
* Localized attribute label for variant dimensions
* Added contentSnippet query for cms-contentful
* Added highlighted specifications configurations for product types
#### Changed
[Section titled “Changed”](#changed-1)
* Cleaned up product variant in Graphql
* Simplified shopping list queries, added expands to all
* Changed cms-contentful and cms-storyblok to return footer.paymentMethods as ResourceLink
### Platform
[Section titled “Platform”](#platform)
#### Feature
[Section titled “Feature”](#feature-2)
* Implemented new error message structure for graphql mutations
#### Changed
[Section titled “Changed”](#changed-2)
* Fix pipelines running even if building docker container has failed
* Moved ribbon to product variant, added stickerText to commercetools
# 2025.9.0 (2025-09-30)
### Storefront
[Section titled “Storefront”](#storefront)
#### Feature
[Section titled “Feature”](#feature)
* Switch to @storybook/nextjs-vite to improve storybook performance
* Add storybook files for checkout steps
* Add tests for the email step
* Use snippets on PDP
* Add product json-ld to detail pages
#### Changed
[Section titled “Changed”](#changed)
* Replaced hardcoded site rating with mocked data
* Use nodejs runtime for nextjs middleware
* Enable `typedRoutes` in NextJS config
* Build the site using turbopack (now stable)
* Added Optimistic UI updates for sort select
* Fix category being removed from URL after sorting
* Add provided button text to teasers block
* Render availability states correctly
* Fixed content page breadcrumb spacing
* Use product variant for product cards
* Add additional fragment for CategoryFacet
* Updated to nextjs 15.5
* Enhance content snippet handling to prevent infinite re-renders in storybook
* Use vitest to run storybook tests
* Remove unused `@storybook/next`. This was replaced by `@storybook/nextjs-vite`
### Backend
[Section titled “Backend”](#backend)
#### Feature
[Section titled “Feature”](#feature-1)
* Added mocked site rating to header
* Added breadcrumb implementation for contentful content page breadcrumbs
* Handle server shutdowns more elegantly by closing connections and cleaning up resources
* Added filtering on categories
* Added snippet usage to storyblok
#### Changed
[Section titled “Changed”](#changed-1)
* Set default commercetools inventoryMode to `ReserveOnOrder`
* Added handling of additionalAddressInfo field
* Update mapPageLink to support resourceLink mapping
* Fixed commercetools product search ordering
* Fixed checkout-commercetools event type
* Removed redundant product availability states
### Platform
[Section titled “Platform”](#platform)
#### Feature
[Section titled “Feature”](#feature-2)
* Run test:ci with turbo, and merge outputs in script for github action reporting
* Split backend and frontend deployments for AWS
* Split backend and frontend deployments for GCP
* Split backend and frontend deployments for Azure
#### Changed
[Section titled “Changed”](#changed-2)
* Update the docker healthcheck url
#### Security
[Section titled “Security”](#security)
* Set pnpm minimumReleaseAge to 7 days for a bit more protection against npm supply chain attacks
#### Dependency
[Section titled “Dependency”](#dependency)
* Update to Sentry 10.x
* Update to pino 9.9 and fix errors in the logger usage which now resulted into errors
* Update dependencies to latest version
* Install correct version of playwright dependencies in CICD pipelines
* Update various dependencies
* Upgrade node docker versions to 24.8
* Update federated-token pkg to latest
# 2026.1.0 (2026-01-30)
### Storefront
[Section titled “Storefront”](#storefront)
#### Feature
[Section titled “Feature”](#feature)
* Added Evolve chat widget
# 2026.2.0 (2026-03-02)
Includes the mcp-core package to allow creating mcp servers from graphql, and an mcp-customer guide implementation How to:
### Storefront
[Section titled “Storefront”](#storefront)
#### Changed
[Section titled “Changed”](#changed)
* Enhanced Contentful live preview integration
### Backend
[Section titled “Backend”](#backend)
#### Feature
[Section titled “Feature”](#feature)
* Added mcp-core package to build mcp servers.
* Added mcp-customer implementation
#### Security
[Section titled “Security”](#security)
* Updated graphql introspection settings to hide schema details from error in production
# 2026.3.0 (2026-04-02)
### Storefront
[Section titled “Storefront”](#storefront)
#### Feature
[Section titled “Feature”](#feature)
* Product comparison page
#### Changed
[Section titled “Changed”](#changed)
* Updated next to 16.1
### Backend
[Section titled “Backend”](#backend)
#### Feature
[Section titled “Feature”](#feature-1)
* Added mollie payment provider
#### Changed
[Section titled “Changed”](#changed-1)
* Updates to running mcp-core package
### Platform
[Section titled “Platform”](#platform)
#### Feature
[Section titled “Feature”](#feature-2)
* Use namespace runners for heavy workloads, use namespace features for caching etc
# 2026.4.0 (2026-04-30)
### Storefront
[Section titled “Storefront”](#storefront)
#### Changed
[Section titled “Changed”](#changed)
* Reorder children and availability rendering in ProductCard
### Backend
[Section titled “Backend”](#backend)
#### Feature
[Section titled “Feature”](#feature)
* MCP Core package implementation
#### Changed
[Section titled “Changed”](#changed-1)
* Updated discount code resolver to return line item discounts
#### Security
[Section titled “Security”](#security)
* Updated axios to fix CVE-2026-40175 and CVE-2025-62718
### Platform
[Section titled “Platform”](#platform)
#### Changed
[Section titled “Changed”](#changed-2)
* Use tsgo for faster type-checking
#### Security
[Section titled “Security”](#security-1)
* Added zizmor and pinact workflows to prevent malicious github action usages
* Updated @fastify/http-proxy and @opentelemetry/auto-instrumentations-node
#### Dependency
[Section titled “Dependency”](#dependency)
* Use pnpm workspace catalogs for version management
* update usage of evolve-platform/ct-client-secret/azurerm module
# 2026.5.0 (2026-05-26)
### Storefront
[Section titled “Storefront”](#storefront)
#### Feature
[Section titled “Feature”](#feature)
* Simplified checkout conversion handling
### Backend
[Section titled “Backend”](#backend)
#### Feature
[Section titled “Feature”](#feature-1)
* Add option to create order before or after payment
* set CT categories limit during load
#### Changed
[Section titled “Changed”](#changed)
* Removed sending metrics to honeycomb (not being processed by collector)
# 2026.6.0 (2026-06-29)
This month marks the first tagged release of the Evolve Framework packages: the shared backend logic of the platform (GraphQL modules, resolvers, dataloaders, mappers, and the server runtime they plug into) distributed as versioned `@evolve-framework/*` packages instead of copied into every project.
### Framework packages
[Section titled “Framework packages”](#framework-packages)
#### Feature
[Section titled “Feature”](#feature)
* Added core (shared server runtime and module system)
* Added schemas (shared GraphQL schema definitions)
* Added commercetools (commercetools commerce engine)
* Added payment adapters for Adyen, Buckaroo, Mollie, PayNL, and Stripe
* Added commercetools-invoice (invoice payments)
* Added contentful and storyblok (CMS integrations)
* Added sfcc (Salesforce Commerce Cloud integration)
* Added mcp-core, with trusted-proxy authentication
* Added cloud adapters for AWS, GCP, and Azure
* Added packager (package build tooling)
* Added json-schema-to-zod
* Added user-notifications
#### Changed
[Section titled “Changed”](#changed)
* Cleaned up core exports
* Cleaned up commercetools and algolia exports
* Updated dependencies
# 2026.7.0 (2026-07-31)
This month marks the start of the Evolve Storefront packages: a set of ready-to-use frontend feature packages (account, orders, organizations, invoices, returns, quotes, auth) that teams can compose a storefront from instead of building every screen themselves. This is an alpha release — expect the APIs to keep changing.
### Storefront package
[Section titled “Storefront package”](#storefront-package)
#### Feature
[Section titled “Feature”](#feature)
* Added feature-account (profile, credentials, addresses)
* Added feature-order (order history and detail)
* Added feature-organization (associates management)
* Added order approval add-on for organizations
* Added feature-invoice
* Added feature-return
* Added feature-quote
* Added feature-auth (login, register, password reset)
* Added B2B multi-list shopping lists
* Added Breadcrumbs primitive with schema.org JSON-LD
* Added PasswordStrengthMeter primitive
* Added active-aware NavItem primitive
#### Changed
[Section titled “Changed”](#changed)
* Migrated all forms to React Aria Components and Zod
* Reworked shopping-list into a composable ShoppingListView
* Merged the features workspace into packages
### Framework packages
[Section titled “Framework packages”](#framework-packages)
#### Feature
[Section titled “Feature”](#feature-1)
* Added a pluggable payment adapter layer
* Added PO reference and delivery tracking to orders
* Added order search
* Added role capabilities and associate status to business units
* Decoupled customer personal details and shipping preferences
* Added per-block content model for Storyblok
* Added Contentful content blocks
* Added the evolve CLI
#### Changed
[Section titled “Changed”](#changed-1)
* Upgraded to TypeScript 7
* Pinned commercetools requests to HTTP/1.1
#### Security
[Section titled “Security”](#security)
* Patched a critical tar advisory
# 2026.8.0 (2026-08-27)
### Storefront package
[Section titled “Storefront package”](#storefront-package)
#### Feature
[Section titled “Feature”](#feature)
* Added feature-cart (active cart)
* Added support for multiple named carts
* Added sticky cart summary
* Added undo on remove from cart
* Added add-to-cart confirmation
* Added section-based checkout flow
* Added pay-later and pay-by-invoice checkout options
* Added Card, Icon, Container, Sheet, and Carousel primitives
* Added loading skeleton for every exported view
* Added nl, de, fr, and es translations to every feature package
#### Changed
[Section titled “Changed”](#changed)
* Rebuilt every overlay on React Aria Components
* Reworked design system color roles, buttons, and tokens
* Show payment status on order, invoice, and quote detail pages
* Simplified the order and quote detail pages
### Framework packages
[Section titled “Framework packages”](#framework-packages)
#### Feature
[Section titled “Feature”](#feature-1)
* Added Medusa as a commerce engine
* Added Meilisearch as a search engine
* Split placing an order from paying for it
* Added pay-on-account credit
* Added approval flows for business unit orders
* Added a company address book
* Added a Payload CMS provider
* Added a Date scalar to the schema
* Unified the pagination shape across the schema
#### Changed
[Section titled “Changed”](#changed-1)
* Reworked invoice settlement to use the shared payment flow
* Made commercetools user session scopes configurable
#### Dependency
[Section titled “Dependency”](#dependency)
* Updated dependencies
* Made core, federated-token, and graphql-yoga peer dependencies
# Getting started
This section of the documentation focuses on working with Evolve. It covers topics as how to use Evolve, how to instantiate it for new projects and design decisions that went into it.
[Installation](/evolve/getting-started/)Installation and setup instructions for developing with Evolve.
[Mastering Evolve technologies](/evolve/mastering-evolve/)Key technologies in Evolve and how to master them.
[Frontend development](/evolve/frontend/)Frontend development guides for the Evolve storefront.
[Backend development](/evolve/backend-services/)How Evolve backend services are built, structured, and extended.
[Continuous Delivery](/evolve/continuous-delivery/)Built-in continuous delivery pipelines through Github Actions.
[Authoring](/evolve/authoring/)AI-driven content authoring layer for Evolve. Editors describe what they want in chat, the AI emits structured page data, the chrome renders it inline on the storefront, and one click pushes the result to your CMS.
[Changelog and versioning](/evolve/changelog/)How Evolve manages changelogs and version numbers.
# Authoring
> Set up and use Evolve's AI-driven content authoring layer. Install the packages, wire up an editor session, and expose the tools to headless clients.
Upcoming
The authoring stack is in active development and not yet part of a platform release. APIs described here may still shift before the first stable release.
Evolve’s authoring layer adds an AI-driven content editor on top of your storefront, shipped as opt-in `@evolve-framework/*` packages. For what it does and how it fits into the platform, see [Authoring](/architecture/authoring/).
This section covers installing and using it.
[Getting started](/evolve/authoring/getting-started/)Install the authoring packages and wire up your first editor session. Takes about ten minutes once your prerequisites are in place.
[Installation guide](/evolve/authoring/installation/)Full installation guide for adding the authoring layer to an existing Evolve project: CMS service, authoring service, gateway, and storefront.
[Packages](/evolve/authoring/packages/)The five @evolve-framework/\* packages that make up the authoring layer, what each one does, and how they depend on each other.
[MCP endpoint](/evolve/authoring/mcp/)Expose the authoring server's AI tools over MCP so headless clients (Claude Desktop, Cursor, agents) can drive the same surface as the in-browser chat, codegen'd from a project-owned GraphQL operations file.
[Workflows](/evolve/authoring/workflows/)Optional opt-in addon. Long-running, headless content workflows triggered by API or MCP that land drafts in your CMS for editorial review. Standard authoring installs don't need it.
# Getting started
> Install the authoring packages and wire up your first editor session. Takes about ten minutes once your prerequisites are in place.
Upcoming
The authoring stack is in active development and not yet part of a platform release. APIs described here may still shift before the first stable release.
This walk-through installs the authoring layer in an existing Evolve project. By the end you can open a page on your storefront, see the editor chrome, and have a chat session that generates and pushes a page to your CMS.
For a production-grade rollout — including the CMS-service module, the gateway plugin, and the storefront wiring in full — follow the [installation guide](/evolve/authoring/installation/); this page is the quick tour.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* An Evolve project with a working storefront and federation gateway
* A CMS service (Contentful) mounted on the gateway, with the authoring module composed in (`ContentfulAuthoringModule` from `@evolve-framework/contentful/authoring`)
* One AI provider you plan to use for chat (Anthropic, OpenAI, or Google) and an API key for it
* An OAuth app on your CMS provider so editors can sign in with their own permissions (Contentful: Developers > OAuth applications)
## 1. Install the packages
[Section titled “1. Install the packages”](#1-install-the-packages)
Install the four base packages in your monorepo. Pick the workspace that runs your authoring backend (a service following the standard Evolve service anatomy) and the one that runs your storefront (typically a Next.js app):
```bash
# In the authoring backend service
pnpm add @evolve-framework/authoring-core \
@evolve-framework/authoring-server \
@evolve-framework/authoring-ai-providers
# In the storefront (Next.js)
pnpm add @evolve-framework/authoring-core \
@evolve-framework/authoring-chrome
```
For headless workflows, also add `@evolve-framework/authoring-workflows` in the backend service. You can defer that until you actually want the addon: the chrome works without it.
## 2. Configure environment variables
[Section titled “2. Configure environment variables”](#2-configure-environment-variables)
You need at least:
* **AI provider**: `CHAT_PROVIDER` (one of `anthropic`, `openai`, `google`), `CHAT_MODEL` (a model id), and the matching API key (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GOOGLE_API_KEY`). `PROVIDER` sets a default for all modalities at once.
* **CMS access**: the editor’s own OAuth token covers reads and writes; the OAuth app credentials (`CONTENTFUL_OAUTH_CLIENT_ID`, `CONTENTFUL_OAUTH_CLIENT_SECRET`) live on the CMS service, which exchanges auth codes on the authoring server’s behalf
* **Internal secret**: `AUTHORING_INTERNAL_SECRET`, a shared secret between the authoring service and the CMS service’s internal OAuth endpoints
* **Session signing**: two 32-byte keys, `SESSION_ENCRYPT_KEY` and `SESSION_SIGN_KEY`, used to encrypt and sign the editor session cookie. Generate with `openssl rand -hex 16`. These must be identical across the authoring service, the storefront, and the gateway — all three read the same cookie.
Optional, for AI image and video generation: `IMAGE_PROVIDER`, `IMAGE_MODEL`, `VIDEO_PROVIDER`, `VIDEO_MODEL` (falling back to `PROVIDER` when unset).
## 3. Run codegen
[Section titled “3. Run codegen”](#3-run-codegen)
`@evolve-framework/authoring-core/codegen` is a graphql-codegen plugin that walks the fragments in your project’s documents and emits a runtime schema registry the AI uses to know which fields exist on which content types.
Add it to your storefront’s `codegen.ts`:
```ts
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "../../backend/services/graphql-gateway/supergraph.generated.graphql",
documents: ["src/**/*.{ts,tsx}"],
generates: {
"./src/authoring/generated/schema-registry.generated.ts": {
plugins: ["@evolve-framework/authoring-core/codegen"],
},
},
};
export default config;
```
Also generate the same registry into your authoring backend service so both sides describe the same shapes. The simplest path is a second `generates` entry in the same codegen config that writes to the service’s source tree:
```ts
"../../backend/services/authoring/src/generated/schema-registry.generated.ts": {
plugins: ["@evolve-framework/authoring-core/codegen"],
},
```
Run `pnpm codegen` to produce the files. They are deterministic from a single codegen run, so the two outputs cannot drift.
If you plan to expose tools over the [MCP endpoint](/evolve/authoring/mcp/) (opt-in but recommended), add a second pair of codegen configs in the authoring service that compile an `authoring-operations.graphql` file into ready-to-mount MCP tools through `@evolve-framework/mcp-core`. The MCP page walks through the full pipeline.
## 4. Mount the server
[Section titled “4. Mount the server”](#4-mount-the-server)
The authoring service follows the standard Evolve service anatomy: a module whose `getHttpConfig()` mounts the editor routes, booted with `createDomainService`. Register the schema registry at startup, then mount the editor with `mountAuthoringEditor`:
src/module.ts
```ts
import {
aiProviderConfigFromEnv,
} from "@evolve-framework/authoring-ai-providers";
import {
mountAuthoringEditor,
registerAuthoringCors,
} from "@evolve-framework/authoring-server";
import { AbstractModule, type HttpConfig } from "@evolve-framework/core";
import { config } from "./config.ts";
export class AuthoringServiceModule extends AbstractModule {
getHttpConfig(): HttpConfig {
return {
routes: (parent): void => {
parent.register(async (app) => {
const applyCorsHeadersRaw = registerAuthoringCors(app, {
allowedOrigins: config.ALLOWED_ORIGINS.split(","),
});
await mountAuthoringEditor(app, {
signerKeys: {
SESSION_ENCRYPT_KEY: config.SESSION_ENCRYPT_KEY,
SESSION_SIGN_KEY: config.SESSION_SIGN_KEY,
JWT_ISSUER: config.JWT_ISSUER,
JWT_AUDIENCE: config.JWT_AUDIENCE,
},
gatewayUrl: config.GATEWAY_URL,
ai: aiProviderConfigFromEnv({
PROVIDER: config.PROVIDER,
CHAT_PROVIDER: config.CHAT_PROVIDER,
CHAT_MODEL: config.CHAT_MODEL,
ANTHROPIC_API_KEY: config.ANTHROPIC_API_KEY,
OPENAI_API_KEY: config.OPENAI_API_KEY,
GOOGLE_API_KEY: config.GOOGLE_API_KEY,
}),
contentful: {
spaceId: config.CONTENTFUL_SPACE_ID,
environmentId: config.CONTENTFUL_ENVIRONMENT,
locale: config.CONTENTFUL_LOCALE,
},
applyCorsHeadersRaw,
});
});
},
};
}
}
```
src/server.ts
```ts
import { createDomainService } from "@evolve-framework/core";
import { setSchemaRegistry } from "@evolve-framework/authoring-core/codegen";
import { authoringSchemaRegistry } from "./generated/schema-registry.generated.ts";
import { AuthoringServiceModule } from "./module.ts";
import { config } from "./config.ts";
export const startServer = async (): Promise => {
setSchemaRegistry(authoringSchemaRegistry);
const service = await createDomainService({
name: config.COMPONENT_NAME,
module: new AuthoringServiceModule(),
http: {
address: { host: config.HTTP_HOST, port: config.HTTP_PORT },
// Chat bodies carry inline image uploads; raise the 1MB default.
bodyLimit: 25 * 1024 * 1024,
},
});
await service.start();
};
```
To enable the bundled OAuth + login UI, pass an `auth` block with the OAuth client id, the CMS service’s internal URL, the shared internal secret, and your cookie domain. Make sure your CORS allowlist includes the storefront origin and that cookies are set on a domain the storefront can read.
The reference implementation lives in `backend/services/authoring/src/module.ts` — including the streaming asset-upload content-type parser and the optional workflows mount.
## 5. Mount the chrome
[Section titled “5. Mount the chrome”](#5-mount-the-chrome)
In your Next.js layout (a server component), wrap the storefront content with `AuthoringServerMount` from the `/server` sub-export. It reads the editor cookie itself and short-circuits to `children` for anonymous shoppers — they pay zero chrome cost. You provide the storefront-specific slots: the CMS config, a page loader, slug routing, and a `bindings` render-prop that bridges the chrome to your project’s router and components:
```tsx
import { AuthoringServerMount } from "@evolve-framework/authoring-chrome/server";
import type { ReactNode } from "react";
import { StorefrontAuthoringBindings } from "#authoring/host-bindings.tsx";
import { loadAuthoringPageByPath } from "#authoring/preview/load-page.ts";
import { storefrontSlugRouting } from "#authoring/routing.ts";
export async function AuthoringChromeMount({
locale,
children,
}: {
locale: string;
children: ReactNode;
}) {
return (
(
{shell}
)}
>
{children}
);
}
```
The bindings provider is where your project’s routing, block renderer, and locale logic plug in — see `frontend/site/src/authoring/host-bindings.tsx` in the platform for the reference implementation.
The chrome ships its own stylesheet baked into the JS chunk, so there is no separate CSS file to import or serve. Shoppers never download the chrome: the editor surface is loaded lazily after the session resolves.
## 6. First editor session
[Section titled “6. First editor session”](#6-first-editor-session)
Restart your services and visit any page on your storefront while signed in. You should see:
1. A profile badge in the top-right corner showing your editor name
2. A floating prompt bar at the bottom of the page, with quick actions to open the chat panel or the page browser
3. A drawer on the left listing the pages, entries, and assets in your CMS
Open the chat panel, type `make this page about summer barbecues`, and watch the page update inline. When you’re happy with the draft, click **Push** to write the result to your CMS.
If you see the chrome but the AI doesn’t respond, check the network tab for the `/chat` request and confirm your AI provider API key is reaching the authoring server. The `/ai/info` endpoint (visible in your Swagger docs) reports which provider and model are active.
## Where to next
[Section titled “Where to next”](#where-to-next)
* [Packages](/evolve/authoring/packages/): what each `@evolve-framework/*` package does in detail, with API surfaces
* [MCP endpoint](/evolve/authoring/mcp/): serve the same tools to headless AI clients with a project-owned `authoring-operations.graphql`
* [Workflows](/evolve/authoring/workflows/) *(optional)*: the opt-in addon for headless, long-running content jobs
* The package READMEs on npm have per-package configuration tables and security notes
# Installation guide
> Full installation guide for adding the authoring layer to an existing Evolve project — CMS service, authoring service, gateway, and storefront.
Upcoming
The authoring stack is in active development and not yet part of a platform release. APIs described here may still shift before the first stable release.
The [getting started](/evolve/authoring/getting-started/) page is the ten-minute tour. This page is the complete installation: every layer an existing Evolve project has to touch before an editor can sign in and push a page. Authoring spans four places in your stack, and each one gets a section below:
1. **The CMS service** — gains the authoring GraphQL surface
2. **The authoring service** — a new service hosting chat, library, and push routes
3. **The gateway** — learns to forward the editor’s CMS token
4. **The storefront** — mounts the editor chrome
## Before you start
[Section titled “Before you start”](#before-you-start)
You will need:
* A Contentful **OAuth application** (Developers → OAuth applications). Its redirect URL points at your authoring service’s callback route.
* An **AI provider key** (Anthropic, OpenAI, or Google) for chat.
* **Local HTTPS with Portless** ([guide](/how-to/local-https-portless/)). This is not optional: the editor session cookie is shared between the storefront, the authoring service, and the gateway on a common parent domain (`*.evolve.localhost` in dev), which requires real hostnames and HTTPS.
### Shared secrets
[Section titled “Shared secrets”](#shared-secrets)
Generate these once and configure them consistently — most installation problems trace back to a mismatch here:
| Secret | Used by | Purpose |
| ---------------------------------------- | -------------------------------------- | ---------------------------------------------- |
| `SESSION_ENCRYPT_KEY` (32 bytes) | authoring service, storefront, gateway | Encrypts the editor session cookie |
| `SESSION_SIGN_KEY` (32 bytes) | authoring service, storefront, gateway | Signs the session data cookie |
| `JWT_ISSUER` / `JWT_AUDIENCE` | authoring service, storefront, gateway | Claims validated on the session token |
| `AUTHORING_INTERNAL_SECRET` | authoring service, CMS service | Protects the internal OAuth exchange endpoints |
| `CONTENTFUL_OAUTH_CLIENT_ID` / `_SECRET` | CMS service | The OAuth app credentials |
The three `SESSION_*`/JWT consumers all read the **same cookie** — if any of them uses different keys, the session silently fails verification.
## 1. The CMS service
[Section titled “1. The CMS service”](#1-the-cms-service)
The authoring GraphQL surface (library search, page push, publish flows) ships as `ContentfulAuthoringModule` in `@evolve-framework/contentful/authoring`. Compose it next to the delivery module in your CMS service:
backend/services/cms-contentful/src/module.ts
```typescript
import { ContentfulModule } from "@evolve-framework/contentful";
import { ContentfulAuthoringModule } from "@evolve-framework/contentful/authoring";
import { CompositeModule } from "@evolve-framework/core";
import { config } from "./config.ts";
export const createModule = (): CompositeModule => {
return new CompositeModule([
new ContentfulModule({
config: {
spaceId: config.CONTENTFUL_SPACE_ID,
environment: config.CONTENTFUL_ENVIRONMENT,
accessToken: config.CONTENTFUL_ACCESS_TOKEN,
previewToken: config.CONTENTFUL_PREVIEW_TOKEN,
managementToken: config.CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN,
},
}),
new ContentfulAuthoringModule({
config: {
spaceId: config.CONTENTFUL_SPACE_ID,
environment: config.CONTENTFUL_ENVIRONMENT,
oauthClientId: config.CONTENTFUL_OAUTH_CLIENT_ID,
oauthClientSecret: config.CONTENTFUL_OAUTH_CLIENT_SECRET,
internalSecret: config.AUTHORING_INTERNAL_SECRET,
},
}),
]);
};
```
Add the three new fields to the service’s config class as **optional** env fields — the service still boots without them, and the OAuth routes return `503` until they are configured.
What this module brings:
* The `authoring*` queries and mutations on the CMS subgraph. Every resolver requires the editor’s CMS token (forwarded by the gateway as `X-Authoring-Cms-Token`) and rejects with `REQUIRES_AUTHORING_ACCESS` otherwise — public storefront traffic is unaffected.
* The `/internal/oauth/contentful/*` REST routes that exchange and refresh editor OAuth tokens on behalf of the authoring service, guarded by `AUTHORING_INTERNAL_SECRET`.
Whether authoring runs embedded in the CMS service (shown above) or as its own subgraph is a deploy-time choice — drop the module from the composite and mount it in a separate service to split it out.
Finally, recompose the supergraph so the gateway knows the new fields:
```bash
task -d backend/services/graphql-gateway supergraph
```
## 2. The authoring service
[Section titled “2. The authoring service”](#2-the-authoring-service)
Create a new service (the reference uses port `4007`) following the standard service anatomy. Its module mounts the editor routes, the AI tools, and optionally the MCP endpoint and workflows — the [getting started](/evolve/authoring/getting-started/#4-mount-the-server) page shows the module code in full.
Configuration highlights beyond the shared secrets:
| Variable | Example (dev) | Purpose |
| ---------------------------------------- | ------------------------------------- | ------------------------------------------ |
| `GATEWAY_URL` | `http://localhost:4000/graphql` | Where authoring tools execute GraphQL |
| `ALLOWED_ORIGINS` | `https://storefront.evolve.localhost` | CORS allowlist for the chrome |
| `AUTHORING_SERVER_URL` | `https://authoring.evolve.localhost` | Public URL of this service |
| `STOREFRONT_BASE_URL` | `https://storefront.evolve.localhost` | Where login redirects back to |
| `AUTHORING_COOKIE_DOMAIN` | `evolve.localhost` | Parent domain the session cookie is set on |
| `CMS_CONTENTFUL_INTERNAL_URL` | `http://localhost:4004` | The CMS service’s internal OAuth routes |
| `CHAT_PROVIDER` / `CHAT_MODEL` + API key | `anthropic` / model id | The chat model |
Two integration points people forget:
* **Body limit**: chat requests carry inline image uploads; raise the HTTP `bodyLimit` (the reference uses 25 MB) in `createDomainService`.
* **Local development**: register the service in the monolith (`backend/services/monolith/src/index.ts`, import + `moduleNames`) and add a Portless alias (`portless alias authoring.evolve 4007` via the service’s `setup:portless` script).
## 3. The gateway
[Section titled “3. The gateway”](#3-the-gateway)
The gateway resolves the editor’s session into a CMS token and forwards it to subgraphs as `X-Authoring-Cms-Token`. This is the `authoring_token` router plugin, configured in the router config:
backend/services/graphql-gateway/router.config.yaml (excerpt)
```yaml
plugins:
authoring_token:
enabled: true
config:
issuer: "${JWT_ISSUER}"
audience: "${JWT_AUDIENCE}"
encrypt_keys:
- id: "1"
secret: "${AUTHORING_SESSION_ENCRYPT_KEY}"
sign_keys:
- id: "1"
secret: "${AUTHORING_SESSION_SIGN_KEY}"
```
The `AUTHORING_SESSION_*` values must equal the authoring service’s `SESSION_*` keys. Two delivery paths flow through this plugin:
* **Header path**: the authoring service proxies chat/library/push calls and sets `X-Authoring-Cms-Token` itself (it decrypted the cookie server-side). The plugin passes it through.
* **Cookie path**: the chrome calls the gateway directly from the storefront origin with the `__authoring_session` cookie; the plugin decrypts it and extracts the CMS token.
An invalid or expired session never fails the request — the plugin simply forwards no token, and the CMS resolvers answer `REQUIRES_AUTHORING_ACCESS`. The chrome reacts by refreshing the session and retrying.
Also on the gateway:
* **CORS**: add the storefront origin to the router’s CORS policy — the chrome makes credentialed cross-origin calls.
* **Production only**: the router enforces persisted documents (`require_id: true`), so the chrome’s GraphQL operations must be registered as Hive App Deployment document IDs alongside your storefront’s.
* The router only reads its config at boot — restart it after changes.
## 4. The storefront
[Section titled “4. The storefront”](#4-the-storefront)
Install `@evolve-framework/authoring-core` and `@evolve-framework/authoring-chrome`, then:
1. **Mount the chrome** in the layout that wraps your main pages, via a wrapper around `AuthoringServerMount` — see [getting started](/evolve/authoring/getting-started/#5-mount-the-chrome). The mount reads the session cookie server-side and renders nothing for anonymous shoppers.
2. **Host bindings**: implement the bindings provider that bridges the chrome to your router, block renderer, and locale logic. The platform reference is `frontend/site/src/authoring/host-bindings.tsx`.
3. **Proxy handling**: the storefront’s `src/proxy.ts` must let `/authoring` paths through before i18n routing and expose the requested path to the chrome (an `x-pathname` header in the reference).
4. **Schema registry codegen**: add the `@evolve-framework/authoring-core/codegen` plugin to your codegen config ([getting started](/evolve/authoring/getting-started/#3-run-codegen)).
5. **Dev origins**: with Portless, add the aliases to `allowedDevOrigins` in `next.config.ts` — Next.js otherwise blocks cross-origin dev resources, which silently prevents all client hydration (the chrome then never appears):
frontend/site/next.config.ts (excerpt)
```typescript
const nextConfig: NextConfig = {
allowedDevOrigins: ["storefront.evolve.localhost", "*.evolve.localhost"],
// ...
};
```
Environment: the storefront needs `AUTHORING_SERVER_URL`, the gateway URL, and the same `SESSION_*` / JWT values as the authoring service (it verifies the session cookie during server rendering).
## 5. Verify the installation
[Section titled “5. Verify the installation”](#5-verify-the-installation)
Work through these in order — each step isolates a layer:
1. **Boot**: `pnpm dev` — the monolith log should show the authoring service starting alongside the others.
2. **Guard**: query the gateway without a session:
```bash
curl -s localhost:4000/graphql -H 'content-type: application/json' \
-d '{"query":"{ authoringListPages { id } }"}'
```
Expect a `REQUIRES_AUTHORING_ACCESS` error — that proves the CMS module is mounted and enforcing.
3. **Login**: visit the authoring service’s login route, complete the Contentful OAuth round-trip, and confirm the `__authoring_session` cookie is set on the parent domain (`Domain=evolve.localhost`).
4. **Chrome**: open the storefront — profile badge, prompt bar, and drawer should appear.
5. **End to end**: open the drawer (library search exercises the cookie→gateway→CMS path), then generate and **Push** a page.
### Common failure modes
[Section titled “Common failure modes”](#common-failure-modes)
| Symptom | Likely cause |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Cookie set, but no chrome | Storefront can’t verify the session: `SESSION_*`/JWT mismatch with the authoring service, or blocked dev origins (check `allowedDevOrigins`) |
| Chrome visible, gateway calls fail with CORS errors | Storefront origin missing from the router CORS policy, or the router wasn’t restarted after the config change |
| `REQUIRES_AUTHORING_ACCESS` while logged in | Gateway plugin keys differ from the authoring service’s `SESSION_*` keys, so the cookie decrypts to nothing |
| OAuth exchange returns `503` | OAuth credentials or `AUTHORING_INTERNAL_SECRET` not configured on the CMS service |
| Chat works but pushes fail | The editor’s Contentful user lacks management permissions on the space |
# MCP endpoint
> The authoring server exposes its AI tools over MCP so headless clients (Claude Desktop, Cursor, agents) can drive the same surface as the in-browser chat. Tools are codegen'd from a project-owned GraphQL operations file via @evolve-framework/mcp-core.
Upcoming
The authoring stack is in active development and not yet part of a platform release. APIs described here may still shift before the first stable release.
`@evolve-framework/authoring-server` mounts an opt-in `/mcp` route alongside the existing `/chat` route. Same tools, same auth, same session — different transport. Headless AI clients connect to it the way `mcp-customer` already does for the storefront’s commerce surface.
The tools the endpoint serves are **not** hardcoded in the package. The framework ships a small set of built-ins (page generation, image and video generation, block-path lookup) and the host project owns the GraphQL-backed ones via an `authoring-operations.graphql` file that gets compiled to typed MCP tools by `@evolve-framework/mcp-core`’s codegen.
## Why a project-owned operations file
[Section titled “Why a project-owned operations file”](#why-a-project-owned-operations-file)
Every Evolve project has a slightly different commerce schema, content model, and AI surface. Hardcoding the GraphQL queries in the package would force every customer onto the reference shape — and would mean shipping a new package version every time anyone wanted to extend the toolset.
Putting `authoring-operations.graphql` in the host service flips the ownership: the framework ships the tool runtime and the built-ins, the project ships its own queries. Codegen turns those queries into typed tool definitions at build time, so no GraphQL strings ride along in the published packages.
It’s the same pattern `@evolve-platform/mcp-customer` already uses for the storefront. The authoring layer now reuses `@evolve-framework/mcp-core`’s codegen pipeline directly — see [mcp-core](/architecture/mcp/mcp-core/) for the directive reference.
## Tool split
[Section titled “Tool split”](#tool-split)
The 14 tools the authoring AI uses break down into two groups:
| Side | Tools | Why |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **Framework built-ins** | `generate-page`, `patch-page`, `find-block-path`, `generate-image`, `generate-video` | Pure functions on `PageData` or AI provider calls. No GraphQL, no customer schema. |
| **Project-owned (codegen)** | `search-products`, `search-categories`, `search-entries`, `search-assets`, `load-page`, `push-page`, `prepare-publish`, `publish-page`, `catalog` | Commerce/CMS schema is customer-specific. Each project tunes the queries to its own page types and content model. |
The framework set is mounted via `buildBuiltinAuthoringTools`. The project set is mounted via `createAuthoringToolFromGenerated` over the codegen output.
## Architecture
[Section titled “Architecture”](#architecture)
```
graph LR
Headless["Claude Desktop / Cursor / agents"] --> MCP
Chrome["In-browser chat authoring-chrome"] --> Chat
Chat["/chat — ai-sdk"] --> Tools["Shared tool runtime"]
MCP["/mcp — mcp-core"] --> Tools
Tools --> Gateway[GraphQL gateway]
Tools --> AI[AI providers]
```
`/chat` and `/mcp` live side-by-side on the same Fastify host and share the same tool implementations. The chrome keeps using `/chat` (streaming SSE for the chat panel); headless clients use `/mcp`.
## Codegen pipeline
[Section titled “Codegen pipeline”](#codegen-pipeline)
The project runs two codegen passes against the federation gateway’s supergraph. Both live in the host service (typically `backend/services/authoring/`):
1. **`codegen.ts`** — produces typed documents and persisted-document hashes for every operation in `authoring-operations.graphql`. Uses the `mcpToolTransform` from `@evolve-framework/mcp-core/codegen` so the client-preset learns to read the `@mcpTool` directive and carry MCP metadata through to the next pass.
2. **`codegen.tools.ts`** — reads the persisted-documents output of pass one and emits `src/generated/authoring-tools.generated.ts` with the `generatedMcpTools` array, ready to mount.
codegen.ts
```ts
import { mcpToolTransform } from "@evolve-framework/mcp-core/codegen";
import type { CodegenConfig } from "@graphql-codegen/cli";
export const documentPaths = ["./authoring-operations.graphql"];
export const schemaPaths = [
"../graphql-gateway/supergraph.generated.graphql",
"./node_modules/@evolve-framework/mcp-core/directives.graphql",
];
const config: CodegenConfig = {
schema: schemaPaths,
generates: {
"./src/generated/": {
documents: documentPaths,
preset: "client",
documentTransforms: [mcpToolTransform],
presetConfig: {
fragmentMasking: false,
persistedDocuments: {
mode: "embedHashInDocument",
hashPropertyName: "documentId",
},
},
},
},
};
export default config;
```
codegen.tools.ts
```ts
import type { CodegenConfig } from "@graphql-codegen/cli";
import { documentPaths, schemaPaths } from "./codegen.ts";
const config: CodegenConfig = {
schema: schemaPaths,
documents: documentPaths,
generates: {
"./src/generated/authoring-tools.generated.ts": {
plugins: ["@evolve-framework/mcp-core/codegen/tools"],
config: {
persistedDocumentsPath: "./src/generated/persisted-documents.json",
},
},
},
};
export default config;
```
Both passes run from `pnpm codegen`.
## Defining a tool
[Section titled “Defining a tool”](#defining-a-tool)
Every operation in `authoring-operations.graphql` becomes a typed MCP tool through two directives:
```graphql
query SearchProducts(
$searchTerm: String
@mcpToolVariable(
description: "Search term, e.g. 'sneakers' or 'winter coat'."
)
$categoryId: String
@mcpToolVariable(
description: "Filter by category id. Call search-categories first for a valid id."
)
$pageSize: Int! = 8
)
@mcpTool(
description: "Search products in the commerce catalog. Use when a ProductBlock needs SKUs."
) {
productSearch(
searchTerm: $searchTerm
categoryId: $categoryId
pageSize: $pageSize
) {
total
results {
variant { sku name primaryImage { url } }
}
}
}
```
The framework ships an `examples/authoring-operations.graphql` with the standard Evolve toolset as a starting point. Copy it, prune the tools you don’t want, and tune the queries to your own content shape.
## Mounting the endpoint
[Section titled “Mounting the endpoint”](#mounting-the-endpoint)
Mount the MCP route on the same Fastify host as the editor server, after `mountAuthoringEditor` so the session signer is ready:
```ts
import {
aiTextOneShot,
generateImageBytes,
generateVideoBytes,
} from "@evolve-framework/authoring-ai-providers";
import {
buildBuiltinAuthoringTools,
createAuthoringToolFromGenerated,
mountAuthoringEditor,
registerAuthoringMcp,
} from "@evolve-framework/authoring-server";
import { generatedMcpTools } from "./generated/authoring-tools.generated.ts";
await mountAuthoringEditor(app, { /* ...existing config */ });
registerAuthoringMcp(app, {
tools: [
...buildBuiltinAuthoringTools({
gatewayUrl: config.GATEWAY_URL,
locale: config.CONTENTFUL_LOCALE,
contentfulSpaceId: config.CONTENTFUL_SPACE_ID,
imageGenerator: generateImageBytes,
videoGenerator: generateVideoBytes,
aiText: aiTextOneShot,
}),
...generatedMcpTools.map((tool) =>
createAuthoringToolFromGenerated(tool, {
gatewayUrl: config.GATEWAY_URL,
locale: config.CONTENTFUL_LOCALE,
}),
),
],
gatewayEndpoint: config.GATEWAY_URL,
});
```
That’s it: the endpoint serves the built-ins, your project’s generated tools, and shares the session machinery `/chat` already uses.
## Authentication
[Section titled “Authentication”](#authentication)
`/mcp` accepts the same two credentials the rest of the authoring server understands:
* **`__authoring_session` cookie** — the browser path. The MCP inspector and any in-page tooling pick this up automatically once the editor is signed in.
* **`X-Authoring-Cms-Token` header** — the headless path. The value can be either an editor’s CMS OAuth token (paste once into the Claude Desktop config) or a service-account CMS management token (the same token the workflows addon uses for unattended runs).
No separate PAT infrastructure: the existing CMS token is the identity carrier. When it expires, the editor pastes a fresh one.
## Headless client setup
[Section titled “Headless client setup”](#headless-client-setup)
For Claude Desktop, point the MCP client at the authoring service’s `/mcp` URL with the CMS token as a header. Example fragment for `claude_desktop_config.json` against a local dev environment:
```json
{
"mcpServers": {
"evolve-authoring": {
"url": "https://authoring.evolve.localhost/mcp",
"headers": {
"X-Authoring-Cms-Token": ""
}
}
}
}
```
For local development the URL above lines up with the [Portless](/how-to/local-https-portless/) setup that gives the authoring service a stable HTTPS hostname.
## Side-by-side with `/chat`
[Section titled “Side-by-side with /chat”](#side-by-side-with-chat)
The existing `/chat` ai-sdk route is unchanged. The chrome continues to use it for the streaming chat panel because ai-sdk’s streaming contract is what the chrome’s transcript renderer consumes. `/mcp` is purely additive: turn it off (don’t call `registerAuthoringMcp`) and the chrome works exactly as before.
The two routes share the same underlying tool implementations through a small adapter layer, so a fix to a tool lands in both transports at once.
## Where to next
[Section titled “Where to next”](#where-to-next)
* [mcp-core](/architecture/mcp/mcp-core/) — directive reference, type mapping, query-security model.
* [mcp-customer](/architecture/mcp/mcp-customer/) — the reference MCP service that uses the same codegen pipeline for storefront commerce tools.
* [Packages](/evolve/authoring/packages/) — `authoring-server` API surface.
# Packages
> The five @evolve-framework/* packages that make up the authoring layer, what each one does, and how they depend on each other.
Upcoming
The authoring stack is in active development and not yet part of a platform release. APIs described here may still shift before the first stable release.
The authoring layer ships as five npm packages in the `@evolve-framework/*` scope. Four are required for a standard install, one is an opt-in addon for headless workflows.
```
graph TD
Core[authoring-core types, queries, codegen]
Chrome[authoring-chrome React editor UI]
Server[authoring-server Fastify HTTP routes]
Providers[authoring-ai-providers LLM + image + video]
Workflows[authoring-workflows opt-in addon]
Chrome -- peer --> Core
Server -- peer --> Core
Server -- peer --> Providers
Workflows -- peer --> Server
Workflows -- peer --> Providers
Workflows -- peer --> Core
```
## `@evolve-framework/authoring-core`
[Section titled “@evolve-framework/authoring-core”](#evolve-frameworkauthoring-core)
Universal types, GraphQL queries, session helpers, and page/block schemas shared across the rest of the stack. Side-effect free, so it is safe in both browser and Node.
What you get:
* **Shared types**: `PageData`, `BlockData`, locale and slug helpers in `./types/*`
* **Typed GraphQL operations**: `./library/types` and `./library/queries` for CMS reads
* **Session helpers**: FederatedToken cookie helpers and signer in `./auth` and `./auth/server`
* **AI tool schemas**: zod-based tool input schemas plus the schema-prompt builder shared by the chat agent and workflows
* **Codegen plugin**: a graphql-codegen plugin at `./codegen` that walks fragments in your project’s documents and emits a runtime `authoringSchemaRegistry` plus a typed `gateway-schema.ts` of zod validators
Every other authoring package declares `authoring-core` as a peer dependency. Keep the major version in sync across the set.
## `@evolve-framework/authoring-chrome`
[Section titled “@evolve-framework/authoring-chrome”](#evolve-frameworkauthoring-chrome)
React UI surface for the editor: drawer, content-console (chat panel), push dialog, page renderer, editor overlays, `AutoEditable`, host bindings, and preview helpers.
What you mount:
* **``**: client component that wraps your app shell with the chat panel and the editor drawer
* **``**: server component for Next.js layouts that gate visibility on the session
* **Bindings**: a typed DI surface (`useRouter`, `localizedPathFor`, `RenderBlock`, schema registry, feature flags) you wire in once per project
The chrome bundles its own stylesheet into the JS chunk: no separate CSS file to symlink or serve. Shoppers never download the chrome because the shell is loaded lazily through `next/dynamic`.
Peer dependencies: React 19, react-dom 19, Next.js 15+, `tailwindcss` (optional, only if your storefront already uses it for layout).
## `@evolve-framework/authoring-server`
[Section titled “@evolve-framework/authoring-server”](#evolve-frameworkauthoring-server)
Fastify plugin that exposes the HTTP route surface the chrome calls: streaming chat, library reads, push to CMS, image and video generation, suggest-prompt, skills, and preview. Optionally also mounts an MCP endpoint for headless AI clients.
```ts
import {
mountAuthoringEditor,
registerAuthoringMcp,
buildBuiltinAuthoringTools,
createAuthoringToolFromGenerated,
} from "@evolve-framework/authoring-server";
await mountAuthoringEditor(app, {
signerKeys,
gatewayUrl: config.GATEWAY_URL,
ai: buildAiProviderConfig(),
contentful: { spaceId, environment, locale },
});
// Optional, opt-in: serve the same tools over MCP.
registerAuthoringMcp(app, {
tools: [
...buildBuiltinAuthoringTools({ /* ... */ }),
...generatedMcpTools.map((tool) =>
createAuthoringToolFromGenerated(tool, { /* ... */ }),
),
],
gatewayEndpoint: config.GATEWAY_URL,
});
```
What you mount on a Fastify host. The package owns its route handlers, pre-handler auth, and OpenAPI metadata. Your host wires:
* **CORS hooks**: origin allowlist for the chrome’s cross-origin calls
* **Swagger plugin**: tags and spec mount, so the editor’s routes show up in your API docs
* **Body limits and content-type parsers**: for the upload-asset endpoint
The `/mcp` route is opt-in — don’t call `registerAuthoringMcp` and the server behaves exactly as before. When you do mount it, tools come from a project-owned `authoring-operations.graphql` compiled by `@evolve-framework/mcp-core`’s codegen, plus a small set of framework built-ins. See [MCP endpoint](/evolve/authoring/mcp/) for the full pipeline.
Peer dependencies: `authoring-core`, `authoring-ai-providers`, `fastify`, `fastify-plugin`. The MCP surface adds a peer dep on `@evolve-framework/mcp-core`.
## `@evolve-framework/authoring-ai-providers`
[Section titled “@evolve-framework/authoring-ai-providers”](#evolve-frameworkauthoring-ai-providers)
LLM, image, and video provider adapters. Anthropic, OpenAI, and Google for chat. fal and Google Gemini for image and video. Plus an editorial agent and a campaign planner used by the workflows addon.
```ts
import { buildAiProviderConfig } from "@evolve-framework/authoring-ai-providers";
const ai = buildAiProviderConfig({
AI_CHAT_PROVIDER: "anthropic",
AI_CHAT_MODEL: "claude-sonnet-4",
AI_IMAGE_PROVIDER: "fal",
AI_IMAGE_MODEL: "fal-ai/flux/dev",
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
FAL_KEY: process.env.FAL_KEY,
});
```
Conditional exposure: a deployment without an image API key loads fine, the AI just doesn’t see the `generateImage` tool. Same for video. Chat is required.
## `@evolve-framework/authoring-workflows` (optional)
[Section titled “@evolve-framework/authoring-workflows (optional)”](#evolve-frameworkauthoring-workflows-optional)
**Opt-in addon.** A standard authoring install (chrome + server + AI providers) does not need this package. Add it only when you want headless, long-running content jobs — campaign hubs, bulk page generation, scheduled drafts triggered by API or MCP — to land in your CMS for editorial review.
Inngest engine, Redis app-state, a workflow definitions registry, and a natural-language compiler that turns plain-English briefs into typed workflow definitions.
```ts
import { mountWorkflows } from "@evolve-framework/authoring-workflows";
mountWorkflows(app, {
triggerToken: config.WORKFLOW_TRIGGER_TOKEN,
redisUri: config.REDIS_URL,
inngestEventKey: config.INNGEST_EVENT_KEY,
inngestSigningKey: config.INNGEST_SIGNING_KEY,
validateEditorSession,
generatePageFromBrief,
savePageDraft,
planCampaign,
buildToolContext,
});
```
Mount on the same Fastify host as `authoring-server` — the workflows reuse its session machinery and AI providers through the host-injected callbacks. The chrome detects the addon through the `features.workflows` binding: turn it off (or simply don’t install the package) to hide the workflows UI on hosts that don’t need it.
Peer dependencies: `authoring-core`, `authoring-server`. Optional peer: `authoring-ai-providers` (only needed for the NL compiler and default flows). See [Workflows](/evolve/authoring/workflows/) for the full walk-through.
# Workflows
> Optional opt-in addon. Long-running, headless content workflows triggered by API or MCP that land drafts in your CMS for editorial review. Standard authoring installs don't need it.
Upcoming
The authoring stack is in active development and not yet part of a platform release. APIs described here may still shift before the first stable release.
`@evolve-framework/authoring-workflows` is an **opt-in addon** for the authoring stack. The chrome, the chat panel, the MCP endpoint, the push-to-CMS flow — none of that depends on this package. Install it only when you have a use case for **headless, long-running content production** that doesn’t fit the interactive chat surface.
Opt-in module
A standard install (`authoring-core` + `authoring-chrome` + `authoring-server` + `authoring-ai-providers`) gives you the full editor experience. The workflows addon is a separate npm package, a separate Fastify mount, and a separate set of environment variables. If you don’t install it, the chrome hides its workflows UI and the rest of the stack behaves identically.
## When you want this
[Section titled “When you want this”](#when-you-want-this)
Reach for the workflows addon when content production needs to run **without an editor sitting in front of a chat panel**. Concrete cases:
* **Bulk page generation**: 200 SEO landing pages overnight from a CSV of keywords.
* **Campaign hubs**: one brief expands into a hub page plus a fan-out of child pages, all landing as drafts ready for editorial review.
* **Scheduled content**: a workflow triggered by a cron or external system that generates content at a specific time.
* **External integration**: another system (PIM, planning tool, marketing automation) posts a brief and waits for a draft.
If your need is “a human editor opens the chat panel and asks for a page”, you don’t need this — `/chat` already does that, and [`/mcp`](/evolve/authoring/mcp/) extends it to headless AI clients in the same interactive shape.
## How it fits
[Section titled “How it fits”](#how-it-fits)
```
graph LR
Trigger["External trigger API / MCP / chrome"] --> Routes
Routes["/workflows/*"] --> Engine["Inngest engine"]
Engine --> Flows["Flow definitions"]
Flows --> Callbacks["Host callbacks generatePage, savePage, ..."]
Callbacks --> AI["AI providers"]
Callbacks --> CMS["CMS via gateway"]
Engine --> Redis[("Redis run state + audit")]
```
The package mounts its routes on the same Fastify host as `authoring-server`, adding:
* `POST /workflows/runs` — trigger a run via shared bearer token or editor session.
* `GET /workflows/runs[/:id]` — read run status and trace.
* `GET /workflows/definitions` — list available flows.
* `POST /workflows/compile` — turn a natural-language brief into a typed workflow definition via the AI provider.
The actual content production happens through **host-injected callbacks** — the workflows package owns orchestration (Inngest steps, retries, run state in Redis, audit trace), the host owns the domain-specific operations (AI generation, CMS writes, tool context). That keeps the package CMS-agnostic and lets each project plug in its own AI orchestrator.
## Install
[Section titled “Install”](#install)
```bash
pnpm add @evolve-framework/authoring-workflows
```
Peer deps: `@evolve-framework/authoring-server` and `@evolve-framework/authoring-core` (same major). Optional: `@evolve-framework/authoring-ai-providers` (only needed for the natural-language compiler and the default flows).
You also need a running Inngest (self-hosted or the dev server) and Redis. Both already standard for most Evolve deployments.
## Mount
[Section titled “Mount”](#mount)
Mount on the same Fastify host as `authoring-server`, after `mountAuthoringEditor` so the session signer is initialised:
```ts
import { mountWorkflows } from "@evolve-framework/authoring-workflows";
import { readSessionFromRequest } from "@evolve-framework/authoring-server";
mountWorkflows(app, {
triggerToken: config.WORKFLOW_TRIGGER_TOKEN,
redisUri: config.REDIS_URL,
inngestEventKey: config.INNGEST_EVENT_KEY,
inngestSigningKey: config.INNGEST_SIGNING_KEY,
// Editor-cookie validator for chrome-driven "+ New run".
validateEditorSession: async (req) =>
(await readSessionFromRequest(req)) !== null,
// Host-injected operations — see workflow-callbacks.ts.
generatePageFromBrief,
savePageDraft,
planCampaign,
buildToolContext,
});
```
The reference monorepo wires the callbacks against `@evolve-framework/authoring-server/lib` and `@evolve-framework/authoring-ai-providers` in `backend/services/authoring/src/workflow-callbacks.ts`. Copy that as a starting point and adapt to your AI surface.
## Configuration
[Section titled “Configuration”](#configuration)
| Field | Required | Purpose |
| ----------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `triggerToken` | yes (or `validateEditorSession`) | Shared bearer for external systems hitting `POST /workflows/runs`. Set to `null` to disable the external trigger. |
| `redisUri` | yes | Run records, draft inventory, audit log. |
| `inngestEventKey` | yes | Inngest event key (self-hosted or dev server). |
| `inngestSigningKey` | yes | Inngest signing key. |
| `inngestServeHost` | optional | Override the URL the SDK self-reports. Set to `http://host.docker.internal:` when Inngest runs in Docker. |
| `validateEditorSession` | optional | Editor-cookie validator for the chrome’s “+ New run” button. |
| `generatePageFromBrief` | optional | Host AI orchestrator. Omit on viewer-only hosts. |
| `savePageDraft` | optional | Host CMS writer. |
| `planCampaign` | optional | Host campaign planner. |
| `buildToolContext` | optional | Host factory for the DSL runner’s tool context. |
The package refuses to serve `/workflows/*` when neither `triggerToken` nor `validateEditorSession` is configured — at least one credential path must be defined.
## Chrome integration
[Section titled “Chrome integration”](#chrome-integration)
The chrome detects the workflows surface through the `features.workflows` binding:
```tsx
```
Turn it off (or omit the binding) on hosts that don’t run the addon and the workflows UI disappears from the chrome.
## OpenAPI
[Section titled “OpenAPI”](#openapi)
The package exports an `AUTHORING_WORKFLOWS_OPENAPI_FRAGMENT` that spreads into your `@fastify/swagger` config alongside `AUTHORING_SERVER_OPENAPI_FRAGMENT`. The `workflows` tag and the trigger-token bearer scheme show up automatically in your API docs.
## Security
[Section titled “Security”](#security)
The workflows endpoints can land drafts in your CMS through service-account auth — treat them with the same care as any other write surface:
* Rotate `triggerToken` via env reload + redeploy.
* Apply network-level isolation when exposing `/workflows/runs` to untrusted networks.
* Inngest signs all inbound requests via `inngestSigningKey`.
The full security contract lives in the [security RFC](https://github.com/labdigital-evolve/evolve-platform/blob/main/docs/authoring/security-rfc.md) in the platform repo (available once the authoring branch lands on `main`).
## Where to next
[Section titled “Where to next”](#where-to-next)
* [MCP endpoint](/evolve/authoring/mcp/) — the interactive headless surface, the natural counterpart to workflows.
* [Packages](/evolve/authoring/packages/) — the wider package surface.
# Backend development
> How Evolve backend services are built, structured, and extended.
Evolve’s backend is a set of microservices that each own a specific domain: accounts, catalog, checkout, orders, content, payments, and more. These services are composed into a single API through [GraphQL Federation](/architecture/core/graphql/).
Each service runs as a standalone process with its own GraphQL subgraph schema, REST endpoints, and infrastructure configuration. In production they run as containers (or serverless functions), while in development the monolith runner starts all services in a single process for convenience.
## Service structure
[Section titled “Service structure”](#service-structure)
Every service follows the same layout:
* backend/services/\[service-name]/
* src/
* config.ts Environment variable parsing
* module.ts Framework module composition
* server.ts DomainService creation and startup
* context.ts GraphQL context factory
* index.ts Entry point (starts the server)
* testing/ Test mocks and fixtures
* …
* schema.generated.graphql Federation subgraph schema
* codegen.ts GraphQL code generation config
* build.ts Build script
* run.ts Dev server entry point
* terraform/ Infrastructure per cloud provider
* …
The core files (`config.ts`, `module.ts`, `server.ts`, `index.ts`) follow a consistent pattern across all services (see `backend/services/quotes-commercetools` for a canonical example):
* **`config.ts`**: a config class (e.g. extending `CommercetoolsConfig`) that parses environment variables
* **`module.ts`**: a `createModule()` factory that composes framework modules into a `CompositeModule`
* **`server.ts`**: calls `createDomainService()` from `@evolve-framework/core` to combine the module’s GraphQL subgraph (via GraphQL Yoga) and HTTP endpoints into a single server
* **`index.ts`**: the entry point that starts the server
* **`context.ts`**: builds the per-request GraphQL context with store context, client context, DataLoaders, and authentication
## GraphQL modules
[Section titled “GraphQL modules”](#graphql-modules)
Services organize their GraphQL resolvers and type definitions into modules using the framework’s `AbstractModule` base class. Modules are composed together with `CompositeModule`:
```typescript
import { QuoteModule } from "@evolve-framework/commercetools";
import { CompositeModule } from "@evolve-framework/core";
export const createModule = (): CompositeModule => {
return new CompositeModule([new QuoteModule()]);
};
```
The framework provides default modules for common domains (customer, cart, catalog, checkout, order, payment, and more). Your project can use these as-is, override individual resolvers, or add entirely new modules. See the [framework customization docs](/architecture/core/framework/customization/) for details.
## Shared infrastructure
[Section titled “Shared infrastructure”](#shared-infrastructure)
Several patterns are shared across all services through the framework packages:
* **Client factory**: creates and caches commercetools API clients with retry logic, timeouts, and authentication middleware
* **[Token management](/architecture/infrastructure/authentication/)**: handles OAuth flows for anonymous sessions, customer tokens, and API client credentials through the federated token system
* **DataLoaders**: request-scoped batch loaders for efficient commercetools queries (products, categories, carts, orders, etc.) with optional Redis-backed caching
* **Store context**: extracts store key, locale, and currency from request headers and makes them available throughout the request lifecycle
* **Cache**: Redis-backed caching (with in-memory fallback) via `@evolve-framework/core/cache`, used for client context, business logic, and DataLoader results
* **Process management**: graceful startup and shutdown handling through the `ProcessManager` class
## REST endpoints
[Section titled “REST endpoints”](#rest-endpoints)
In addition to their GraphQL subgraph, services can expose REST endpoints through Fastify. See [REST endpoints & webhooks](/architecture/infrastructure/rest-webhooks/) for the full architectural overview. Common uses include:
* **Token endpoints**: anonymous token creation, session management
* **Webhooks**: payment provider callbacks, CMS webhooks
* **API extensions**: commercetools API extension handlers
* **Domain-specific routes**: payment flows, feed generation
## Infrastructure
[Section titled “Infrastructure”](#infrastructure)
Each service includes a `terraform/` directory with per-cloud provider configurations (AWS, Azure, GCP). This covers container definitions, environment variables, IAM roles, and any service- specific infrastructure. The Terraform code is deployed through MACH Composer or directly through CI/CD pipelines.
## Available services
[Section titled “Available services”](#available-services)
Evolve ships with the following services:
| Service | Domain |
| -------------------------------- | --------------------------------------------------------- |
| `account-commercetools` | Customer accounts, authentication, sessions |
| `authoring` (upcoming) | Editor backend for the AI-driven authoring layer |
| `catalog-commercetools` | Product catalog, search sync, sitemaps |
| `chat` | AI shopping-assistant backend (uses `mcp-customer` tools) |
| `checkout-commercetools` | Cart operations, order creation, payment routing |
| `cms-contentful` | Contentful content integration |
| `cms-storyblok` | Storyblok content integration with live editing |
| `email-smtp` | Transactional email with React Email |
| `event-router` | Cloud event routing topology (Terraform-only) |
| `feed-products` | Product feed generation |
| `graphql-gateway` | Federation gateway (Hive Router) |
| `mcp-customer` | MCP server exposing customer-facing commerce tools |
| `monitoring` | Health checks and observability |
| `monolith` | Dev-only runner that starts all services in one process |
| `order-commercetools` | Order retrieval and management |
| `payment-commercetools-adyen` | Adyen payments |
| `payment-commercetools-buckaroo` | Buckaroo payments |
| `payment-commercetools-invoice` | Invoice payments |
| `payment-commercetools-mollie` | Mollie payments |
| `payment-commercetools-paynl` | PayNL payments |
| `payment-commercetools-stripe` | Stripe payments |
| `pim-bluestone-commercetools` | PIM sync from Bluestone PIM to commercetools |
| `quotes-commercetools` | B2B quote management |
When creating a new project, remove the services you don’t need and keep only what’s relevant for your commerce and content setup.
[Backend testing](/evolve/backend-services/testing/)Testing patterns for backend services: Vitest, Fishery factories, MSW, and CommercetoolsMock.
[Checkout](/evolve/backend-services/checkout/)How the checkout domain handles cart operations, order creation, and payment routing.
[Content management](/evolve/backend-services/content/)How Evolve integrates with content management systems to deliver pages, navigation, and site layout through the GraphQL schema.
# Checkout
> How the checkout domain handles cart operations, order creation, and payment routing.
The checkout domain covers everything from adding items to a cart through to completing an order and processing payment. It is split across two types of services: the checkout service (which owns the flow) and payment provider services (which handle PSP communication).
## Cart operations
[Section titled “Cart operations”](#cart-operations)
The checkout service exposes GraphQL mutations for managing the shopping cart:
* **`cartLineItemsAdd`**: add products to the cart by SKU and quantity. Creates a new cart automatically if none exists.
* **`cartLineItemsUpdate`**: change the quantity of existing line items.
* **`cartLineItemsRemove`**: remove line items from the cart.
* **`cartUpdate`**: apply configuration changes such as setting the customer email, billing address, shipping address, shipping method, or payment method.
* **`cartDiscountCodeAdd`** / **`cartDiscountCodeRemove`**: manage discount codes on the cart.
All cart mutations use the `@requiresSession` directive, which creates a guest session automatically when no session exists.
## API extensions
[Section titled “API extensions”](#api-extensions)
The checkout service registers commercetools API extensions that run automatically when resources are created or updated. These extensions handle defaults that would otherwise require extra roundtrips from the frontend:
* **Cart create**: copies the customer’s default addresses and email onto the cart. For B2B carts, it uses the business unit’s addresses instead.
* **Cart update**: auto-selects a default shipping method when a shipping address is set but no method has been chosen.
* **Order create**: generates a unique order number, ensures the customer email is set, and applies the default order type.
* **Payment create**: transitions new payments to the `PaymentInitial` state.
## Order creation
[Section titled “Order creation”](#order-creation)
The `checkoutComplete` mutation validates the cart and creates an order from it. The order extension fires during creation to assign an order number and fill in any missing fields. On success, the mutation returns the order ID and order number.
## Payment routing
[Section titled “Payment routing”](#payment-routing)
When the frontend calls `createPayment`, the checkout service creates a Payment object on the order and delegates the actual transaction to the appropriate payment provider service based on the selected payment method. The provider starts a transaction with the PSP and returns a redirect URL for the customer.
See [Payment architecture](/architecture/payment/) for the full payment flow, state machine, and REST endpoint contract.
## Event subscriptions
[Section titled “Event subscriptions”](#event-subscriptions)
The checkout service subscribes to commercetools messages for asynchronous processing:
* **`PaymentStatusStateTransition`**: when a payment reaches success, the checkout service checks whether all payments on the order cover the total amount and marks the order as paid.
* **`OrderStateChanged`**: when an order is confirmed, the service sends a confirmation email through the notification service.
# Content management
> How Evolve integrates with content management systems to deliver pages, navigation, and site layout through the GraphQL schema.
Evolve integrates with headless CMS platforms through dedicated CMS services. Each CMS service translates vendor-specific content into a shared GraphQL schema, so the frontend and other consumers work with the same types regardless of which CMS is behind them.
Out of the box, Evolve ships with services for [Storyblok](https://www.storyblok.com/) and [Contentful](https://www.contentful.com/). The architecture makes it straightforward to add support for other CMS platforms.
## How it works
[Section titled “How it works”](#how-it-works)
Every CMS service follows the same internal pattern:
```
graph LR
CMS["CMS API (Storyblok, Contentful)"] --> Loaders
Loaders --> Mappers
Mappers --> Resolvers
Resolvers --> Schema["GraphQL subgraph"]
```
* **Loaders**: fetch content from the CMS API using DataLoaders for batching and caching. Results are cached with a TTL (typically 5 minutes) and scoped by store context.
* **Mappers**: convert vendor-specific data structures into the shared GraphQL types. Each CMS has its own mappers for assets, pages, navigation, and layout components.
* **Resolvers**: expose the mapped content through the GraphQL schema. Resolvers are the same across CMS implementations since they work with the shared types.
## Shared schema
[Section titled “Shared schema”](#shared-schema)
Both CMS services expose the same GraphQL types:
* **`AbstractPage`**: base interface for all pages with fields for path, translated paths, breadcrumbs, and SEO metadata
* **`ContentPage`**: pages built from CMS content blocks (about, contact, landing pages)
* **`CatalogPage`**: pages that combine CMS content with product data (category pages, brand pages)
* **`PageMeta`**: SEO metadata (title, description, keywords, OpenGraph data)
Key queries include `page(path)` for fetching any page by its URL path, `siteLayout()` for header and footer content, and `contentSnippet()` for reusable content blocks.
## Webhooks
[Section titled “Webhooks”](#webhooks)
Each CMS service receives webhooks when content is published or updated. The webhook handler invalidates the relevant cache entries so that fresh content is served on the next request without waiting for the TTL to expire.
## Adding a new CMS
[Section titled “Adding a new CMS”](#adding-a-new-cms)
To integrate a different CMS platform, create a new service that implements the same GraphQL schema. You need:
1. Vendor-specific type definitions for the CMS API responses
2. Loaders that fetch content from the CMS API
3. Mappers that convert vendor types to the shared GraphQL types
4. A webhook handler for cache invalidation
The resolvers and GraphQL schema can be reused from the existing CMS services since they operate on the shared types.
# Content Set up
There are many flavors and ways to structure Storyblok content types. This documentation will explain how to structure Storyblok content types in a way that is compatible with Evolve.
## Useful plugins
[Section titled “Useful plugins”](#useful-plugins)
### Advanced paths
[Section titled “Advanced paths”](#advanced-paths)
This plugin allows you to overwrite the path of a story and all child stories in the Storyblok UI. We need this plugin to at least remove the `Pages` folder from the path of all pages. In other words, by default the path of a page would be `/pages/contact`, but with this plugin we can make it `/contact`.
### Translatable slugs
[Section titled “Translatable slugs”](#translatable-slugs)
As the name suggests, this allows you to translate the slug of a story.
### SEO
[Section titled “SEO”](#seo)
This plugin allows you to add SEO meta tags to a story that show a preview of how the page will look like in Google. This is useful since it is advised by SEO experts to have conform to a maximum amount of pixel width for you meta title and description. The preview of this plugin will show you if you are within the limits.
This custom field is supported by default by the GraphQL codegen plugin.
## Common folder and story structures
[Section titled “Common folder and story structures”](#common-folder-and-story-structures)
### Scenario 1 - single site
[Section titled “Scenario 1 - single site”](#scenario-1---single-site)
A simple structure with only 1 (multilingual) site:
```yaml
/
Site # site config contains menu, footer, and other site global manageable data
Pages/
Home # index story
About
Contact
Category/
Category-1/
Category-1 # index story
SubCategory-1
Category-2/
Category-2 # index story
SubCategory-2
...
```
### Scenario 2 - multiple sites
[Section titled “Scenario 2 - multiple sites”](#scenario-2---multiple-sites)
If you need to support multiple sites that have different menus and different page contents, you could add a root folder for each site:
```yaml
/
Site-1/
Site # site config contains menu, footer, and other site global manageable data
Pages/
Site-2/
Site # site config contains menu, footer, and other site global manageable data
Pages/
```
### Scenario 3 - multiple sites with shared pages
[Section titled “Scenario 3 - multiple sites with shared pages”](#scenario-3---multiple-sites-with-shared-pages)
Another scenario is where multiple sites would share the same pages, but have different menus and other site global manageable data.
```yaml
/
Site-1/
Site # site 1 global config
Pages/ # site 1 specific pages
Home
Site-2/
Site # site 2 global config
Pages/ # site 2 specific pages
Home
Shared Pages/
Privacy Policy
```
Whatever setup you would have, make sure to update the pageResolver and the siteResolver inside the Storyblok service accordingly.
### Fixing the paths of the stories
[Section titled “Fixing the paths of the stories”](#fixing-the-paths-of-the-stories)
As described in [Advanced Paths](#advanced-paths) you should remove or edit the realpath for a container folder (e.g. the “Pages” folder). This way the live editing will correctly open the right page, since this is based on the path of the story.
### Create alternate pages
[Section titled “Create alternate pages”](#create-alternate-pages)
Alternate pages are typically required in [Scenario 3](#scenario-3---multiple-sites-with-shared-pages) described above.
Instead of just translating a Page Story, you could also create an entire new story and link them together. This is called an alternate page and can be done under the settings of a Story.
By linking a Story as alternate, it will properly resolve as alternates in the GraphQL resolver. Note that you still should handle the fetching of alternate case in the GraphQL resolver. You should update the pageResolver function so that it does an extra query once it finds the alternate.
### Restrict content types
[Section titled “Restrict content types”](#restrict-content-types)
Good to know is that you can restrict certain content types per folder for the folder and all descendant folders. Therefore you can restrict the `Pages` folder to only contain `Page` content types.
If you would like to manage products or other types of content inside Storyblok as well, you could do something similar with a corresponding root folder.
```yaml
/
Pages/ # Content Page and Category Page only
Category/ # Category Page only
Products/ # Product only
```
It’s also good to know that you can restrict the content types on a Role level. For example you could create a role so users with that Blog authors role can only create Articles inside a Blog folder. This is managed in Settings/Roles.
## Languages
[Section titled “Languages”](#languages)
Languages are set up at space level under Settings/Internationalization. It could be confusing how to configure this correctly, so this section will provide some guidelines.
### Default language
[Section titled “Default language”](#default-language)
It is advised to always choose English as the default language if you support multiple languages. This is because fields are not required by Storyblok to be translated and always will return the fallback language if empty. If you don’t support multiple languages, you can choose any language you want. However keep in mind that if you need to support multiple languages in the future, you probably also should choose English as the default language.
### Locale vs languages
[Section titled “Locale vs languages”](#locale-vs-languages)
Should you use `en` (language) or `en-US` (locale) when configuring languages for the space? Even though there no use case is the same, it is often easier to go for the language only. This is because we often split up sites based on a country.
For example, we could have different texts for Belgian French and French French with the [Scenario 2](#scenario-2---multiple-sites) setup even though we only have 1 French language.
For a [Scenario 1](#scenario-1---single-site) setup that should support both Belgian French and French French, you should configure locales on a space level.
# Creating content types
> How to create and manage Storyblok content types through Terraform.
Storyblok provides a UI for creating content types. However, we do not and should not use that since we manage the types in Terraform. Modifying content types through the UI will result in a mismatch in the Terraform state and break the deploy pipeline.
## How to create a new content type
[Section titled “How to create a new content type”](#how-to-create-a-new-content-type)
Content types are defined as Terraform resources in `backend/configuration/storyblok/`. Each component gets its own `.tf` file:
```hcl
resource "storyblok_component" "hero" {
name = "hero"
space_id = var.storyblok_space_id
is_root = false
is_nestable = true
schema = {
title = {
position = 0
translatable = true
display_name = "Title"
required = true
type = "text"
}
image = {
position = 5
translatable = true
display_name = "Image"
required = true
type = "asset"
filetypes = ["images"]
}
}
}
```
To add a new content type:
1. Create a new `.tf` file in `backend/configuration/storyblok/` (e.g., `component_banner.tf`)
2. Define the `storyblok_component` resource with the fields your content editors need
3. Push the changes through a PR
When the changes are merged, the CI/CD pipeline deploys them to the Storyblok environment. Your changes will not be available in Storyblok during the PR preview stage since Terraform runs on merge.
## TypeScript types
[Section titled “TypeScript types”](#typescript-types)
The TypeScript definitions for Storyblok components ship with the framework: the `@evolve-framework/storyblok` package bundles a `storyblok.types.d.ts` with the component types used by the CMS service. There is no per-project Storyblok type generation step.
The CMS service does have its own codegen for GraphQL and events:
```bash
pnpm codegen
```
In `backend/services/cms-storyblok` this runs `graphql-codegen` for the GraphQL subgraph types and `codegen.types.ts`, which uses `writeZodTypes` from `@evolve-framework/json-schema-to-zod` to generate Zod schemas for platform events into `src/generated/events.ts`.
## Relevant resources
[Section titled “Relevant resources”](#relevant-resources)
* [Storyblok Terraform provider](https://github.com/labd/terraform-provider-storyblok): the Terraform provider that uses the Storyblok management API to manage content types
* [Storyblok Management API](https://www.storyblok.com/docs/api/management): the API schema definitions correspond to the `schema` property of the `storyblok_component` Terraform resource
## Content type vs. nestable component
[Section titled “Content type vs. nestable component”](#content-type-vs-nestable-component)
Storyblok distinguishes between root content types and nestable components:
* **Root content types** (`is_root = true`): can be created as standalone stories. Use these for pages (content pages, catalog pages) and site-level content (header, footer).
* **Nestable components** (`is_nestable = true`): can only be used inside other content types. Use these for reusable blocks (hero, teaser, FAQ item).
A component can be both root and nestable if needed, but in practice you typically choose one.
# How Live Editing in Storyblok works
The live editing setup for Storyblok differs a little from the standard documentation in Storyblok. Many concepts of course still remain because the core functionality is provided by Storyblok itself. However for our setup we have 2 requirements that their tutorials and default solution do not fulfill:
The solution should work with server components. We therefore can not re-render components client side with updated live editing data from Storyblok. This ensures the javascript bundle stays relatively low.
Our solution should work with our custom GraphQL schema.
For a general technical overview from Storyblok, please visit this link:
## Data flow
[Section titled “Data flow”](#data-flow)
The data flow in summary is:
1. A content editor changes some field, adds or removes a blok in the visual editor inside Storyblok.
2. Our site that is loaded in the visual editor inside an iframe receives a postMessage containing the JSON data of the whole updated story (not just the change).
* This data is not saved by Storyblok, it only stores this data if you either save as draft or publish the story.
3. Our site captures this postMessage JSON (client side) and posts it to our GraphQL gateway which temporarily stores it (in memory) and returns a previewId for that entry.
4. We then store this previewId in a cookie and ask NextJS to revalidate the page.
5. NextJS refetches the pages with the previewId as additional parameter.
6. The GraphQL resolver for the page content refetches the page and merges it with the temporarily stored preview JSON and transforms the data into the desired GraphQL schema.
7. The page is now rebuild server side by NextJS with the update from the content editor, and he/she can view the updated page within the iFrame.
* Because of how NextJS revalidation works, this does not result in a page refresh, and therefore feels quite instant.
## Story Selection
[Section titled “Story Selection”](#story-selection)
In order for the visual editor to make the React components selectable, they need to have some data set as data attributes to an HTML element. This includes: the story id, the space id the blok id, and the blok type name. In the end, the HTML will need to look something like the following in order for it to work for Storyblok:
```html
...
```
We can get the space and the story id from the query parameters which are given to the iFrame.
```html
```
We can get the blok id and the blok type from the GraphQL fragment in our queries. Note that in order for this to work, we need to always query the `__typename` and the `id` for a Storyblok component if we want to make it selectable inside the preview environment.
```tsx
type Props = {
data: MyStoryblokFragment; // must include `__typename` and `id`
};
export const MyStoryblokComponent = ({ data }: Props) => (
... some content
);
```
The `componentProps` function sets 2 other more generic properties, namely a `data-component-id` and a `data-component-type`. The idea behind this is that these React components should not be directly linked to Storyblok, since Evolve should be CMS agnostic.
The Storyblok attributes `data-blok-c` and `data-blok-uid` are then set in a `useEffect` hook inside a top-level Storyblok React component called `StoryblokStory.tsx`. This hook queries all elements client side containing a `data-component-id` and adds the two specific Storyblok attributes to it using the query parameters from the iFrame.
### Summary for developers
[Section titled “Summary for developers”](#summary-for-developers)
So in summary, please note that if you are developing a Storyblok component ensure that you:
1. Include the `__typename` and `id` in the Storyblok fragment.
2. Add the Storyblok HTML attributes using the `componentProps` function with the Storyblok fragment as input.
## Debugging Live Editing locally
[Section titled “Debugging Live Editing locally”](#debugging-live-editing-locally)
By default the visual editor is connected to the test environment. However, when something might be broken and we need to figure out what is going on, we can open the visual editor with a local host connection.
For this to work, the local storefront needs to run on https instead of http. Evolve uses [Portless](/how-to/local-https-portless/) for this. Run the one-time setup at the workspace root:
```bash
pnpm dev:setup-portless
```
The storefront (started with `pnpm dev`) is then also available at `https://storefront.evolve.localhost`. See [Local HTTPS with Portless](/how-to/local-https-portless/) for details, or the “Running the dev env on HTTPS” section in the `frontend/site` README.
Then you can select the `https://storefront.evolve.localhost` environment in Storyblok in the top left corner of the visual editor.
# Backend testing
> Testing patterns for backend services: Vitest, Fishery factories, MSW, and CommercetoolsMock.
Evolve backend services use Vitest as the test runner, Fishery for test data factories, and MSW with CommercetoolsMock for HTTP mocking. This guide covers the setup, patterns, and worked examples.
## Test stack
[Section titled “Test stack”](#test-stack)
| Tool | Purpose |
| --------------------------------------------------------------- | ----------------------------- |
| [Vitest](https://vitest.dev/) | Test runner and assertions |
| [Fishery](https://github.com/thoughtbot/fishery) | Type-safe test data factories |
| [MSW](https://mswjs.io/) | HTTP request interception |
| [CommercetoolsMock](https://github.com/labd/commercetools-mock) | In-memory commercetools API |
## Setup
[Section titled “Setup”](#setup)
### vitest.config.ts
[Section titled “vitest.config.ts”](#vitestconfigts)
Each service has a Vitest config that sets up path aliases and global test configuration:
vitest.config.ts
```typescript
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
setupFiles: ["./vitest.setup.ts"],
},
});
```
### vitest.setup.ts
[Section titled “vitest.setup.ts”](#vitestsetupts)
The setup file initializes the MSW server with CommercetoolsMock handlers, stubs environment variables, and resets state between tests:
vitest.setup.ts
```typescript
import { rewindFactorySequences } from "@evolve-framework/commercetools/factories";
import { ctMock } from "@evolve-framework/commercetools/testing";
import { afterAll, afterEach, beforeAll } from "vitest";
import { mswServer } from "#src/testing/mocks.ts";
beforeAll(() => {
mswServer.listen({ onUnhandledRequest: "warn" });
});
afterEach(() => {
mswServer.resetHandlers();
ctMock.clear();
rewindFactorySequences();
});
afterAll(() => {
mswServer.close();
});
```
The `rewindFactorySequences()` call resets Fishery’s auto-increment counters so each test starts from `sequence: 1`.
## MSW and CommercetoolsMock
[Section titled “MSW and CommercetoolsMock”](#msw-and-commercetoolsmock)
CommercetoolsMock provides an in-memory implementation of the commercetools API. Its request handlers are loaded into MSW so that any `fetch()` call to the commercetools API is intercepted and handled in-process.
```typescript
import { CommercetoolsMock } from "@labdigital/commercetools-mock";
import { setupServer } from "msw/node";
export const ctMock = new CommercetoolsMock({
apiHost: "http://localhost",
authHost: "http://localhost",
enableAuthentication: true,
validateCredentials: false,
defaultProjectKey: "testing",
silent: true,
strict: true,
});
export const mswServer = setupServer(...ctMock.getHandlers());
```
To add custom MSW handlers for non-commercetools endpoints (e.g. external APIs), use `mswServer.use()`:
```typescript
import { http, HttpResponse } from "msw";
mswServer.use(
http.post("https://external-api.example.com/webhook", () => {
return HttpResponse.json({ ok: true });
}),
);
```
## Factories
[Section titled “Factories”](#factories)
The framework ships a set of Fishery factories for commercetools resources, importable from `@evolve-framework/commercetools/factories`.
### Available factories
[Section titled “Available factories”](#available-factories)
`associateRoleDraftFactory` `businessUnitDraftFactory` `cartDiscountDraftFactory` `cartDraftFactory` `categoryDraftFactory` `channelDraftFactory` `customerDraftFactory` `discountCodeDraftFactory` `messageDraftFactory` `orderDraftFactory` `paymentDraftFactory` `productDiscountDraftFactory` `productDraftFactory` `productTypeDraftFactory` `shippingMethodDraftFactory` `shoppingListDraftFactory` `standalonePriceDraftFactory` `stateDraftFactory` `storeDraftFactory` `taxCategoryDraftFactory` `zoneDraftFactory`
### `.build()` vs `.create()`
[Section titled “.build() vs .create()”](#build-vs-create)
* **`.build()`** returns a plain object in memory, useful when you only need the data shape without persisting it
* **`.create()`** posts the draft to CommercetoolsMock via the `onCreate` hook and returns the created resource with a generated `id`, `version`, and timestamps
```typescript
// In-memory only - no API call
const draft = customerDraftFactory.build({ email: "test@example.com" });
// Persisted to CommercetoolsMock - has id, version, etc.
const customer = await customerDraftFactory.create({ email: "test@example.com" });
```
Use `.createList(n)` to create multiple resources at once:
```typescript
const products = await productDraftFactory.createList(5);
```
### Customizing factories
[Section titled “Customizing factories”](#customizing-factories)
Override any field by passing attributes:
```typescript
const customer = await customerDraftFactory.create({
email: "vip@example.com",
firstName: "Jane",
lastName: "Doe",
});
```
Factories use `sequence` for auto-incrementing values (e.g. `customer-{sequence}@example.com`), ensuring unique values across tests.
## Context helpers
[Section titled “Context helpers”](#context-helpers)
The framework’s testing utilities provide helpers to create realistic GraphQL context objects for resolver tests. `mockGraphQLContext`, `createAnonymousToken`, and `createCustomerToken` are exported from `@evolve-framework/commercetools/testing`; the context-value helpers (`testContextValue`, `mockContextValue`, `createTestStoreContext`) live in the framework’s `src/testing/` directory and are used by the framework’s own resolver tests, where most resolvers now live.
### `testContextValue()`
[Section titled “testContextValue()”](#testcontextvalue)
Creates a full `ContextValue` with data loaders, store context, client context, and federated token:
packages/commercetools/src/testing/context-value.ts
```typescript
const context = await testContextValue();
// context.storeContext → { storeKey: "test", locale: "en-GB", currency: "EUR" }
// context.federatedToken → anonymous token
```
Pass a customer to get an authenticated context:
```typescript
const customer = await customerDraftFactory.create();
const context = await testContextValue(customer);
```
### `mockContextValue()`
[Section titled “mockContextValue()”](#mockcontextvalue)
Similar to `testContextValue()` but accepts a business unit for B2B scenarios:
```typescript
const context = await mockContextValue({
customer,
businessUnit: await businessUnitDraftFactory.create(),
});
```
### `mockGraphQLContext()`
[Section titled “mockGraphQLContext()”](#mockgraphqlcontext)
Returns a mock context with custom client factories, useful when you need to control the commercetools client behaviour directly:
```typescript
const { storeContext, clientContext } = await mockGraphQLContext({
customer,
clientFactory: () => customRequestBuilder,
});
```
### `createTestStoreContext()`
[Section titled “createTestStoreContext()”](#createteststorecontext)
Creates a standalone store context for tests that don’t need the full GraphQL context:
```typescript
const storeContext = createTestStoreContext({
storeKey: "de-store",
locale: "de-DE",
currency: "EUR",
});
```
## Auth helpers
[Section titled “Auth helpers”](#auth-helpers)
Two helpers create federated tokens for anonymous and authenticated sessions:
```typescript
import {
createAnonymousToken,
createCustomerToken,
} from "@evolve-framework/commercetools/testing";
const anonToken = createAnonymousToken();
// sub: "anonymous_id:1234"
const customer = await customerDraftFactory.create();
const customerToken = createCustomerToken(customer);
// sub: "customer_id:{customer.id}"
```
## Worked examples
[Section titled “Worked examples”](#worked-examples)
### DataLoader test
[Section titled “DataLoader test”](#dataloader-test)
```typescript
import { storeDraftFactory } from "@evolve-framework/commercetools/factories";
import { clientTestFactory } from "@evolve-framework/commercetools/testing";
import { beforeAll, beforeEach, describe, expect, test } from "vitest";
describe("storeByKey", () => {
let loader;
let stores;
beforeAll(() => {
const ctApi = clientTestFactory.getSystemRequestBuilder();
loader = createStoreByKeyLoader(ctApi);
});
beforeEach(async () => {
stores = await storeDraftFactory.createList(2);
});
test("loads a store by key", async () => {
const result = await loader.load(stores[0].key);
expect(result?.key).toEqual(stores[0].key);
});
test("returns null for non-existing key", async () => {
const result = await loader.load("non-existing-key");
expect(result).toBeNull();
});
});
```
### GraphQL resolver test
[Section titled “GraphQL resolver test”](#graphql-resolver-test)
Resolver tests live alongside the resolvers in the framework modules, where `testContextValue` is imported from the local testing utilities:
```typescript
import { customerDraftFactory } from "@evolve-framework/commercetools/factories";
import { testContextValue } from "#src/testing/context-value.ts";
describe("customer resolver", () => {
test("returns the current customer", async () => {
const customer = await customerDraftFactory.create();
const context = await testContextValue(customer);
const result = await resolvers.Query.me({}, {}, context);
expect(result.email).toEqual(customer.email);
});
});
```
### Fastify integration test
[Section titled “Fastify integration test”](#fastify-integration-test)
For testing HTTP endpoints directly, use Fastify’s `inject()` method:
```typescript
import { buildApp } from "../app";
describe("POST /api/webhook", () => {
test("processes a valid webhook", async () => {
const app = await buildApp();
const response = await app.inject({
method: "POST",
url: "/api/webhook",
payload: { event: "payment.completed", id: "tx-123" },
});
expect(response.statusCode).toBe(200);
});
});
```
## Tips
[Section titled “Tips”](#tips)
* Keep factories stateless. Use `.create()` in `beforeEach`, not at module level
* Use `mswServer.use()` for one-off handler overrides in individual tests; they’re automatically cleaned up in `afterEach`
* The `strict: true` option on CommercetoolsMock throws on unexpected API calls, catching accidental requests early
* Run tests with `pnpm test` from the service directory or `pnpm test` at the root to run all backend tests
# Changelog and versioning
> How Evolve manages changelogs and version numbers.
## Overview
[Section titled “Overview”](#overview)
Maintaining a changelog for your codebase is crucial as it provides a clear and structured record of all notable changes made over time. This helps developers, stakeholders, and users to easily track the progress and evolution of the project, understand the context of changes, and identify when specific features or fixes were introduced. Additionally, a well-maintained changelog enhances transparency, facilitates better communication within the team, and aids in troubleshooting by providing a historical reference of modifications.
Our changelog maintenance strategy ensures that all notable changes are documented in a structured and consistent manner. This strategy involves creating individual changelog entry files for each pull request, merging these entries into a single entry, and following a calendar versioning scheme.
## Outline
[Section titled “Outline”](#outline)
### 1. Creating changelog entry files
[Section titled “1. Creating changelog entry files”](#1-creating-changelog-entry-files)
For each pull request, a separate changelog entry file is created. This file should contain a brief description of the changes introduced by the pull request. The file is typically named using the pull request number or a unique identifier to avoid conflicts.
### 2. Merging entries
[Section titled “2. Merging entries”](#2-merging-entries)
We use [Changie](https://changie.dev/) to merge individual changelog entries into a single entry for the given release. Changie automates the process of collecting all changelog entry files and combining them into a comprehensive changelog for the release. This ensures that all changes are documented in a single, cohesive entry.
### 3. Calendar versioning
[Section titled “3. Calendar versioning”](#3-calendar-versioning)
Our project follows a [calendar versioning scheme](https://calver.org/). This means that our version numbers are based on the date of the release. The format is `YYYY.MM.BUILD`, where which makes it easy to identify when a particular version was released.
## Workflow
[Section titled “Workflow”](#workflow)
1. **Create a Changelog Entry**: When a pull request is created, add a new changelog entry file in the `.changes` directory. This can be done by running `pnpm changie new`, which will start an interactive prompt to create the changelog entry.
2. **Automated PR creation**: The CICD pipeline will automatically create or update a release PR once a new changelog entry is added to the `.changes` directory.
3. **Releasing**: Simply merge the created release PR, which will take care of updating `CHANGELOG.md` to reflect the new release.
By following this strategy, the changelog stays up-to-date and provides a clear history of changes for each release.
## Tailoring to your own needs.
[Section titled “Tailoring to your own needs.”](#tailoring-to-your-own-needs)
This strategy is tailored to our specific needs and may need to be adapted to suit your project’s requirements.
In `.changie.yaml` you can configure the changelog generation to suit your needs. For example, you can add or remove change types here, as well as default page elements. See [the documentation](https://changie.dev/config/) for further options
If you prefer tag-based versioning follow the following steps:
* remove `CHANGELOG.md`, `.changie.yaml` and `.changes`
* in `.github/workflows/prepare-release.yaml` remove the `create-version-command` element
* run `pnpm changie init`.
This will recreate all configurations, defaulting to tag-based versioning.
# Continuous Delivery
> Built-in continuous delivery pipelines through Github Actions.
Evolve ships with GitHub Actions pipelines that deliver the platform from day one. When you fork the repository, the workflows come along — this page describes what each one does so you can adapt them to your own environments.
## Overview
[Section titled “Overview”](#overview)
The continuous delivery setup automates the build, test, and deployment process across multiple cloud platforms (AWS, GCP, and Azure).
## Key Components
[Section titled “Key Components”](#key-components)
1. **Monorepo Structure**: the codebase is organized as a monorepo, which allows for easier management of dependencies and shared code across different parts of the platform.
2. **pnpm and Turborepo**: pnpm is the package manager and Turborepo optimizes builds across the workspace.
3. **MACH Composer**: manages and deploys the microservices architecture.
4. **Multi-Cloud Support**: the pipelines support deployments to AWS, GCP, and Azure, as well as specialized platforms like Vercel for frontend hosting.
## Main Workflows
[Section titled “Main Workflows”](#main-workflows)
### CI/CD Pipeline
[Section titled “CI/CD Pipeline”](#cicd-pipeline)
The main CI/CD pipeline (`ci-cd.yaml`) includes the following key steps:
1. **Validation**: code checks (`validate-code.yaml`), dependency review (`validate-dependencies.yaml`), and pull-request standards (`validate-pull-request.yaml`).
2. **Testing**: runs unit tests and reports coverage; Storybook is validated through `validate-storybook.yaml` and `validate-storybook-vitest.yaml`.
3. **MACH Composer Validation**: `validate-mach-composer-config.yaml` checks the configurations for the different cloud environments.
4. **Service-Specific Builds**: the `service-common-*.yaml` and `service-site.yaml` workflows build individual services when their files change.
### Deployments
[Section titled “Deployments”](#deployments)
`deploy-test.yaml` deploys the MACH Composer configurations to the test environment; `deploy-production.yaml` promotes to production. The monolith variant (`deploy-monolith.yaml`) covers single-container deployments.
### Storybook deployment
[Section titled “Storybook deployment”](#storybook-deployment)
A separate workflow (`deploy-storybook.yaml`) handles the deployment of Storybook to ensure the component library is always up-to-date.
## Optimization Strategies
[Section titled “Optimization Strategies”](#optimization-strategies)
1. **Caching**: pnpm, Turborepo, and Terraform plugin caches speed up builds.
2. **Conditional Job Execution**: jobs only run when relevant files have changed, reducing unnecessary builds.
3. **Parallel Execution**: where possible, jobs run in parallel to minimize overall pipeline duration.
## Security Considerations
[Section titled “Security Considerations”](#security-considerations)
1. **CODEOWNERS**: a CODEOWNERS file ensures the right teams review changes to critical parts of the codebase.
2. **Secrets Management**: sensitive information is stored as GitHub Secrets and accessed securely within workflows.
3. **Pull Request Validation**: automated checks (`validate-pull-request.yaml`) ensure pull requests meet standards before merging.
For more detailed information on optimizing deployments as your project grows, please refer to the [Optimizing Deployments](/how-to/optimizing-deployments/) guide.
# Frontend development
> Frontend development guides for the Evolve storefront.
The Evolve frontend is a Next.js 16 application with React 19, Tailwind CSS, and TypeScript. It connects to the backend through the GraphQL gateway and supports server-side rendering, static generation, and client-side interactivity. Linting and formatting are handled by Biome.
## Project structure
[Section titled “Project structure”](#project-structure)
The frontend lives in `frontend/site/` and follows the Next.js App Router conventions:
* frontend/site/
* src/
* app/ App Router pages and layouts
* …
* components/ Shared UI components
* …
* generated/ GraphQL Code Generator output
* …
* hooks/ Shared React hooks
* …
* lib/ Utilities, i18n config, store config
* …
* messages/ Translation files per locale
* …
* public/ Static assets
* …
* next.config.ts Next.js configuration
## Rendering strategy
[Section titled “Rendering strategy”](#rendering-strategy)
The storefront uses a mix of rendering approaches:
* **Static generation with ISR**: product pages, category pages, and content pages are statically generated and revalidated on a regular interval (typically 15 minutes)
* **Server-side rendering**: pages that depend on user context (cart, account, checkout) render on the server per request
* **Client-side interactivity**: add-to-cart actions, search autocomplete, and other interactive elements use client components with optimistic updates
## Component approach
[Section titled “Component approach”](#component-approach)
Components follow a headless UI pattern using React Aria Components and Radix UI primitives for accessibility, styled with Tailwind CSS and composed in the style of shadcn/ui. This gives you full control over markup and styling while maintaining accessible behavior out of the box.
Forms use React Hook Form with Zod validation schemas. Interactive state (cart updates, wishlist changes) uses React 19’s `useActionState` and `useOptimistic` hooks for immediate feedback.
## Data fetching
[Section titled “Data fetching”](#data-fetching)
The frontend fetches data from the GraphQL gateway using generated typed queries. GraphQL Code Generator produces TypeScript types from the supergraph schema, so queries are type-safe end to end.
Server components fetch data directly during rendering. Client components use TanStack Query for cache management and background refetching.
## Key patterns
[Section titled “Key patterns”](#key-patterns)
* **Internationalization**: translations are managed through next-intl with automatic label extraction. See [Translations](/evolve/frontend/translations/) for details.
* **Testing**: components are tested through Storybook with Vitest in browser mode. See [Testing](/evolve/frontend/testing/) for the full strategy.
* **Icons**: Lucide React icons with custom icon generation for project-specific assets.
[Testing](/evolve/frontend/testing/)Testing strategy for the Evolve frontend using Vitest, Storybook, and Playwright.
[Translations](/evolve/frontend/translations/)How translations work in Evolve using next-intl, automatic label extraction, and Crowdin.
# Testing
> Testing strategy for the Evolve frontend using Vitest, Storybook, and Playwright.
Evolve uses Vitest as the test runner for all frontend tests. Depending on what you’re testing, it’s extended with Storybook (component testing) or Playwright (end-to-end and browser mode).
## End-to-end testing
[Section titled “End-to-end testing”](#end-to-end-testing)
E2E tests are handled by [Playwright](https://playwright.dev/) and live in the `frontend/site` directory. They focus on testing complete user flows (checkout, login, search) and smoke tests that verify pages load correctly.
```bash
pnpm e2e
```
## Component testing
[Section titled “Component testing”](#component-testing)
Component testing uses [Storybook](https://storybook.js.org/) with the `@storybook/addon-vitest` test addon. This setup runs Vitest 4 in browser mode with Playwright as the browser provider, giving you a real browser environment instead of JSDOM.
How it works:
* **Stories as test cases**: Storybook automatically tests multiple render states based on your stories
* **Interactions**: additional user interactions (clicks, form input, navigation) are tested using the `play` method on stories
* **Real browser**: tests run in headless Chromium, so you get accurate DOM behavior without the limitations of JSDOM
```bash
pnpm test:storybook
```
For components or hooks that cannot be tested within Storybook, use Vitest with browser mode directly. Avoid JSDOM as it is not a real DOM and often needs excessive mocking for browser APIs.
## Unit testing
[Section titled “Unit testing”](#unit-testing)
Unit tests use Vitest with the Node environment for logic that doesn’t need a browser (utilities, data transformations, helpers):
```bash
pnpm test
```
## Principles
[Section titled “Principles”](#principles)
* **Keep test complexity in tests**: don’t make components harder to work with just to make them more testable. Test setup and assertions belong in the test file, not in production code.
* **Prefer browser mode over JSDOM**: when testing anything that touches the DOM, use Storybook or Vitest browser mode for accurate behavior.
* **Test behavior, not implementation**: focus tests on what the user sees and does, not on internal component state or implementation details.
# Translations
> How translations work in Evolve using next-intl, automatic label extraction, and Crowdin.
Evolve uses [next-intl](https://next-intl.dev/) for internationalization. Translation keys are extracted automatically from the source code, stored as JSON, and synced with Crowdin for professional translation management.
## How it works
[Section titled “How it works”](#how-it-works)
1. You use `useTranslations()` or `getTranslations()` in your components
2. Run `pnpm codegen:messages` to extract all translation keys into `messages/source.json`
3. Crowdin picks up new keys and makes them available for translators
4. A daily CI/CD job downloads translations and opens a PR
## Message files
[Section titled “Message files”](#message-files)
Translation messages live in `frontend/site/src/messages/`:
* messages/
* source.json Extracted keys (source of truth)
* nl-NL.json Dutch translations
* en-GB.json English translations
* de-DE.json German translations
* fr-FR.json French translations
* … Other locales
Messages use a hierarchical structure where the top-level key matches the component namespace:
```json
{
"AccountPage": {
"title": "Hi, {name}",
"addresses": "Your addresses"
},
"CheckoutForm": {
"email": "Email address",
"billing-address": "Billing address"
}
}
```
At runtime, next-intl deep-merges the locale-specific file with `source.json`, so untranslated keys fall back to the source language.
## Using translations in components
[Section titled “Using translations in components”](#using-translations-in-components)
### Server components
[Section titled “Server components”](#server-components)
Use `getTranslations()` from `next-intl/server`. This is async and runs at request time:
```tsx
import { getTranslations } from "next-intl/server";
export default async function AccountPage({ params }) {
const { locale } = await params;
const t = await getTranslations({
namespace: "AccountPage",
locale,
});
return
{t("title", { name: "John" })}
;
}
```
### Client components
[Section titled “Client components”](#client-components)
Use the `useTranslations()` hook:
```tsx
"use client";
import { useTranslations } from "next-intl";
export function CheckoutForm() {
const t = useTranslations("CheckoutForm");
return (
{t("email")}
{t("billing-address")}
);
}
```
The namespace (first argument) must match a top-level key in the message files.
## Extracting translation keys
[Section titled “Extracting translation keys”](#extracting-translation-keys)
The `intl-extractor` tool scans the source code for `useTranslations()` and `getTranslations()` calls and extracts all referenced keys into `source.json`:
```bash
pnpm codegen:messages
```
Run this whenever you add or change translation keys. The extractor generates the complete `source.json` from the source code, so you never need to edit that file manually.
## Locale routing
[Section titled “Locale routing”](#locale-routing)
Locales are configured in the site-config package and used by next-intl’s routing middleware. Each locale gets a URL prefix:
```typescript
export const localePrefixes = {
"nl-NL": "/nl",
"en-GB": "/en",
"de-DE": "/de",
"fr-FR": "/fr",
"fr-BE": "/be/fr",
"nl-BE": "/be/nl",
};
```
Translated pathnames (e.g., `/cart` becoming `/warenkorb` in German) are defined in the same routing configuration.
## Translation management with Crowdin
[Section titled “Translation management with Crowdin”](#translation-management-with-crowdin)
Translations are managed through [Crowdin](https://crowdin.com/). A GitHub Actions workflow runs daily to:
1. Upload `source.json` with any new keys to Crowdin
2. Download completed translations from Crowdin
3. Open a PR with updated locale files
The workflow can also be triggered manually. Translated locale files should not be edited directly in the repository since Crowdin is the source of truth for translations.
## Storybook
[Section titled “Storybook”](#storybook)
Storybook has access to all translations so components render with real text. The `.storybook/next-intl.ts` file loads and merges messages for each locale, matching the runtime behavior.
# Installation
> Installation and setup instructions for developing with Evolve.
Evolve supports development on **macOS**, **Windows (WSL)**, and **Linux** environments. This guide walks you through installation, environment setup, and running the platform locally.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
| Tool | Purpose |
| ---------------------------------------------------------- | ------------------------------- |
| [Node.js 24+](https://nodejs.org/) | JavaScript runtime |
| [pnpm](https://pnpm.io/) (via corepack) | Package manager |
| [go-task](https://taskfile.dev/) | Task runner |
| [1Password CLI](https://developer.1password.com/docs/cli/) | Environment variable management |
| [Docker](https://www.docker.com/) | Local development services |
## Installation
[Section titled “Installation”](#installation)
### macOS
[Section titled “macOS”](#macos)
```bash
brew install node@24 go-task 1password-cli
corepack enable
pnpm install
```
### Windows (WSL)
[Section titled “Windows (WSL)”](#windows-wsl)
1. Install WSL 2 with the default Ubuntu distro:
```bash
wsl --install
```
2. Install [Homebrew](https://brew.sh/), then install dependencies:
```bash
brew install go-task nvm 1password-cli
```
3. Install Node.js 24 and enable corepack:
```bash
nvm install 24
corepack enable
```
### Linux / Ubuntu
[Section titled “Linux / Ubuntu”](#linux--ubuntu)
1. Install [Node.js 24+](https://nodejs.org/en/download/package-manager/all) using a package manager of your choice.
2. Install [1Password CLI](https://support.1password.com/install-linux/#debian-or-ubuntu).
3. Install [Task](https://taskfile.dev/):
```bash
snap install task --classic
```
## Environment setup
[Section titled “Environment setup”](#environment-setup)
Evolve uses 1Password to manage environment variables. A fully working `.env` file is available in your project’s shared vault:
```bash
task get-env
```
## Running the development server
[Section titled “Running the development server”](#running-the-development-server)
```bash
pnpm dev
```
Visit the storefront at .
### Local HTTPS with Portless
[Section titled “Local HTTPS with Portless”](#local-https-with-portless)
For OAuth callbacks, secure cookies, and the authoring chrome’s cross-subdomain session probe, you’ll want real HTTPS. Evolve uses [Portless](https://portless.sh/) to put every service behind a stable `*.evolve.localhost` hostname with a system-trusted cert:
```bash
pnpm dev:setup-portless # one-time per machine
pnpm dev:secure # same as pnpm dev, served over HTTPS
```
See [Local HTTPS with Portless](/how-to/local-https-portless/) for the full walk-through, including how to add new services and the SameSite-cookie reasoning behind the two-level domain.
## Local development services
[Section titled “Local development services”](#local-development-services)
A Docker Compose file provides supporting services for local development:
| Service | URL |
| ---------------------- | ------------------------ |
| Redis | — |
| Jaeger (OpenTelemetry) | |
| Mailpit (local SMTP) | |
```bash
task docker:services # start
docker compose down # stop
```
## Useful commands
[Section titled “Useful commands”](#useful-commands)
| Command | Description |
| ---------------------- | ----------------------------------------------------------- |
| `pnpm dev` | Start the full development environment |
| `pnpm check` | Run formatting, typechecking, linting, and Terraform checks |
| `pnpm test` | Run all tests |
| `pnpm codegen` | Generate GraphQL types and schema files |
| `task get-env` | Pull latest `.env` from 1Password |
| `task docker:services` | Start Redis, Jaeger, and Mailpit |
## API testing with Bruno
[Section titled “API testing with Bruno”](#api-testing-with-bruno)
Example GraphQL Gateway API calls are included in the `api-collection` folder, meant to be used with the [Bruno API Client](https://www.usebruno.com/):
1. Download and install [Bruno](https://www.usebruno.com/downloads).
2. Import the collection from the `api-collection` folder.
3. Select the correct environment (top right corner) and execute calls.
## Next steps
[Section titled “Next steps”](#next-steps)
* Learn about Evolve’s [architecture](/architecture/) and how services are organized.
* Explore the [technology stack](/evolve/mastering-evolve/) to understand the frameworks and tools used.
* Read the [frontend](/evolve/frontend/) and [backend](/evolve/backend-services/) development guides.
## Relevant links
[Section titled “Relevant links”](#relevant-links)
| **Documentation** | **Demo environments** |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| [GitHub](https://github.com/labdigital-evolve/evolve-platform) | [AWS / Commercetools / Storyblok](https://www.aws-tst.evolve.labdigital.nl) |
| [Storybook](https://ui.evolve.labdigital.nl) | [GCP / Commercetools / Storyblok](https://www.gcp-tst.evolve.labdigital.nl) |
| [Figma design system](https://www.figma.com/file/KrXNO5VFWMYqqc4A6Suewx/Lab-Digital---Accelerator) | [Azure / Commercetools / Storyblok](https://www.azure-tst.evolve.labdigital.nl) |
| [Lab Digital engineering docs](https://tech-docs.labdigital.nl) | |
# Mastering Evolve technologies
> Key technologies in Evolve and how to master them.
## Introduction
[Section titled “Introduction”](#introduction)
Evolve is a modern, enterprise-grade composable commerce platform built on MACH architecture principles (Microservices, API-first, Cloud-native, Headless). This guide outlines the key technologies used throughout the platform and provides training resources to help developers master each technology stack.
The platform follows a microservices architecture with a React/Next.js frontend, GraphQL Federation for API orchestration, and multi-cloud deployment capabilities. Whether you’re a frontend developer working on the storefront, a backend developer building microservices, or a DevOps engineer managing infrastructure, this guide will help you identify the skills needed to excel with Evolve.
Evolve’s core logic is consumed as published `@evolve-framework/*` packages, pinned through the pnpm catalog. Your project composes these framework packages into services rather than carrying all the platform code itself, so framework upgrades are dependency version bumps.
***
## Frontend Technologies
[Section titled “Frontend Technologies”](#frontend-technologies)
The Evolve frontend is built with cutting-edge web technologies focusing on performance, accessibility, and developer experience. The stack emphasizes modern React patterns, type safety, and component-driven development.
### Technologies
[Section titled “Technologies”](#technologies)
* **Framework**: Next.js 16 with App Router, React 19
* **UI Components**: React Aria Components, Radix UI primitives, headless UI library, Using Shadcn composable components approach
* **Styling**: Tailwind CSS 4.1, CSS-in-JS with class-variance-authority
* **State Management**: React 19 features (useActionState, useOptimistic), TanStack Query
* **Forms**: React Hook Form with Zod validation
* **Internationalization**: next-intl
* **Testing**: Storybook, Vitest, Playwright, Testing Library
* **Icons & Assets**: Lucide React, custom icon generation
### Training & Certification Resources
[Section titled “Training & Certification Resources”](#training--certification-resources)
* **React**: [React Official Documentation](https://react.dev/), [React Certification by Meta](https://www.credly.com/org/meta/badge/meta-certified-react-developer)
* **Next.js**: [Next.js Learn](https://nextjs.org/learn), [Vercel Ship](https://vercel.com/ship)
* **TypeScript**: [TypeScript Handbook](https://www.typescriptlang.org/docs/), [Microsoft TypeScript Certification](https://docs.microsoft.com/en-us/learn/paths/build-javascript-applications-typescript/)
* **Tailwind CSS**: [Tailwind CSS Documentation](https://tailwindcss.com/docs), [Tailwind UI](https://tailwindui.com/)
* **Testing**: [Testing Library Courses](https://testingjavascript.com/), [Playwright University](https://playwright.dev/docs/intro)
* **Shadcn**: [Shadcn Documentation](https://ui.shadcn.com/)
***
## Backend Technologies
[Section titled “Backend Technologies”](#backend-technologies)
The backend architecture leverages GraphQL Federation to create a unified API from multiple microservices. Each service is built with modern Node.js frameworks and integrates with various third-party platforms for commerce, content management, and payments.
### Technologies
[Section titled “Technologies”](#technologies-1)
* **Runtime**: Node.js 24+
* **GraphQL**: Federation via Hive Router (Rust gateway), GraphQL Yoga (subgraphs)
* **API Framework**: Fastify
* **Payment Processors**: Stripe, Adyen, Buckaroo, PayNL, Invoice
* **Data Loading**: DataLoader pattern for efficient queries
* **Authentication**: Federated tokens, JWT (jose)
### Training & Certification Resources
[Section titled “Training & Certification Resources”](#training--certification-resources-1)
* **Node.js**: [Node.js Documentation](https://nodejs.org/en/docs/), [OpenJS Node.js Certification](https://openjsf.org/certification/)
* **GraphQL**: [GraphQL Foundation](https://graphql.org/learn/), [Apollo GraphQL Certifications](https://www.apollographql.com/tutorials/)
* **Fastify**: [Fastify Documentation](https://www.fastify.io/docs/latest/), [Fastify Ecosystem](https://www.fastify.io/ecosystem/)
***
## Cloud Infrastructure & DevOps
[Section titled “Cloud Infrastructure & DevOps”](#cloud-infrastructure--devops)
Evolve supports deployment across major cloud providers with Infrastructure as Code practices. The platform emphasizes scalability, reliability, and multi-cloud flexibility for enterprise requirements.
### Technologies
[Section titled “Technologies”](#technologies-2)
* **Multi-Cloud**: AWS, Azure, Google Cloud Platform
* **Infrastructure as Code**: Terraform (HashiCorp)
* **Containerization**: Docker, AWS Lambda, Cloud Functions
* **CI/CD**: GitHub Actions with extensive workflow automation
* **Caching**: Redis, Keyv, LRU Cache
* **Message Queuing**: Azure Service Bus, AWS SQS/SNS
### Training & Certification Resources
[Section titled “Training & Certification Resources”](#training--certification-resources-2)
* **AWS**: [AWS Certified Solutions Architect](https://aws.amazon.com/certification/certified-solutions-architect-associate/), [AWS Certified Developer](https://aws.amazon.com/certification/certified-developer-associate/)
* **Azure**: [Azure Fundamentals](https://docs.microsoft.com/en-us/learn/paths/azure-fundamentals/), [Azure Developer Associate](https://docs.microsoft.com/en-us/learn/certifications/azure-developer/)
* **Google Cloud**: [Google Cloud Engineer](https://cloud.google.com/certification/cloud-engineer), [Google Cloud Developer](https://cloud.google.com/certification/cloud-developer)
* **Terraform**: [HashiCorp Certified Terraform Associate](https://www.hashicorp.com/certification/terraform-associate), [Terraform Learn](https://learn.hashicorp.com/terraform)
* **Docker**: [Docker Certified Associate](https://training.mirantis.com/dca-certification-exam/), [Docker Documentation](https://docs.docker.com/)
***
## Observability & Monitoring
[Section titled “Observability & Monitoring”](#observability--monitoring)
The platform implements comprehensive observability with distributed tracing, metrics collection, and error tracking. This ensures production reliability and helps maintain optimal performance across all services.
### Technologies
[Section titled “Technologies”](#technologies-3)
* **Distributed Tracing**: OpenTelemetry (OTLP exporters)
* **Metrics**: OpenTelemetry metrics SDK
* **Logging**: Pino with pretty printing
* **Error Tracking**: Sentry with Next.js and Node.js integrations
* **APM**: Jaeger for trace visualization
* **Schema Monitoring**: GraphQL Hive for schema management
### Training & Certification Resources
[Section titled “Training & Certification Resources”](#training--certification-resources-3)
* **OpenTelemetry**: [OpenTelemetry Documentation](https://opentelemetry.io/docs/), [CNCF OpenTelemetry Training](https://www.cncf.io/certification/training/)
* **Sentry**: [Sentry Documentation](https://docs.sentry.io/), [Sentry Academy](https://sentry.io/welcome/)
* **Observability**: [Observability Engineering Book](https://www.oreilly.com/library/view/observability-engineering/9781492076438/), [SRE Workbook](https://sre.google/workbook/table-of-contents/)
* **Jaeger**: [Jaeger Documentation](https://www.jaegertracing.io/docs/), [Jaeger Tutorial](https://github.com/jaegertracing/jaeger/tree/main/examples/hotrod)
***
## Development Tools & Quality
[Section titled “Development Tools & Quality”](#development-tools--quality)
The development workflow emphasizes code quality, type safety, and developer productivity. Modern tooling ensures consistent code standards and efficient collaboration across teams.
### Technologies
[Section titled “Technologies”](#technologies-4)
* **Package Management**: pnpm workspaces with syncpack
* **Build System**: Turbo (monorepo orchestration), esbuild
* **Code Quality**: Biome (linting/formatting), Prettier (formatting), TypeScript 6.0
* **Testing**: Vitest, MSW (API mocking), Commercetools Mock
* **Security**: CodeQL analysis, dependency scanning
* **Dependency Analysis**: Knip for unused dependencies
### Training & Certification Resources
[Section titled “Training & Certification Resources”](#training--certification-resources-4)
* **TypeScript**: [TypeScript Deep Dive](https://basarat.gitbook.io/typescript/), [Total TypeScript](https://totaltypescript.com/)
* **Testing**: [JavaScript Testing Best Practices](https://github.com/goldbergyoni/javascript-testing-best-practices), [Vitest Guide](https://vitest.dev/guide/)
* **Code Quality**: [Clean Code Principles](https://www.oreilly.com/library/view/clean-code-a/9780136083238/), [ESLint Documentation](https://eslint.org/docs/user-guide/)
* **Security**: [OWASP Top 10](https://owasp.org/www-project-top-ten/), [GitHub Security Lab](https://securitylab.github.com/)
***
## Source Control & Collaboration
[Section titled “Source Control & Collaboration”](#source-control--collaboration)
The platform uses modern Git workflows with automated processes for code review, testing, and deployment. Environment management and secrets handling follow security best practices.
### Technologies
[Section titled “Technologies”](#technologies-5)
* **Version Control**: Git with GitHub
* **CI/CD**: GitHub Actions
* **Code Review**: CODEOWNERS, PR automation
* **Environment Management**: 1Password for secrets, dotenv
### Training & Certification Resources
[Section titled “Training & Certification Resources”](#training--certification-resources-5)
* **Git**: [Pro Git Book](https://git-scm.com/book), [GitHub Skills](https://skills.github.com/)
* **GitHub Actions**: [GitHub Actions Documentation](https://docs.github.com/en/actions), [GitHub Actions Certification](https://docs.github.com/en/actions/learn-github-actions)
* **Security**: [GitHub Advanced Security](https://docs.github.com/en/code-security), [Security Best Practices](https://docs.github.com/en/code-security/getting-started/securing-your-repository)
# FAQ
> Frequently asked questions about Evolve.
## What is Evolve?
[Section titled “What is Evolve?”](#what-is-evolve)
Evolve is a composable commerce platform built on MACH principles (Microservices, API-first, Cloud-native, Headless). It provides a production-ready backend and storefront for B2C and B2B digital commerce, composed from independent domain services behind a unified GraphQL Federation API. Evolve is also built to support AI agents through MCP and GraphQL, making it ready for agentic commerce. See the [introduction](/) for the full overview.
## How does Evolve support AI agent integration?
[Section titled “How does Evolve support AI agent integration?”](#how-does-evolve-support-ai-agent-integration)
Evolve is designed to be agent-friendly at two levels. First, its unified [GraphQL Federation API](/architecture/mcp/why-graphql-federation/) gives AI agents a strongly typed, self-documenting interface to the entire commerce platform. Agents can discover available operations, understand input/output types, and execute queries and mutations without custom glue code.
Second, Evolve has built-in support for the [Model Context Protocol](/architecture/mcp/) (MCP). MCP tools are defined declaratively on top of existing GraphQL operations using `@mcpTool` directives, so any query or mutation can be exposed as an AI tool with automatically generated input schemas. This lets AI assistants browse products, manage carts, look up orders, and more through a standardized protocol. See the [MCP architecture](/architecture/mcp/) docs for details.
## Does Evolve support ACP and UCP?
[Section titled “Does Evolve support ACP and UCP?”](#does-evolve-support-acp-and-ucp)
Not yet, but it is on the near-term roadmap. We are running proof-of-concept implementations for both the Agent Commerce Protocol (ACP) and the Unified Commerce Protocol (UCP). We expect to ship support for both protocols in the coming months.
## How does Evolve relate to commercetools?
[Section titled “How does Evolve relate to commercetools?”](#how-does-evolve-relate-to-commercetools)
commercetools is a commerce engine. It provides the headless APIs for products, carts, orders, and customers. Evolve is the composable commerce platform that turns commercetools (and other MACH services) into a fully working commerce system. Evolve adds the GraphQL Federation API layer, storefront, domain services, payment orchestration, email delivery, AI agent integration, multi-cloud infrastructure, and more. In short: commercetools is one of the building blocks that Evolve composes into a complete platform. See the [introduction](/) for the full picture.
## Why would I use Evolve instead of building from scratch?
[Section titled “Why would I use Evolve instead of building from scratch?”](#why-would-i-use-evolve-instead-of-building-from-scratch)
Evolve gives you a working commerce platform from day one: checkout, payments, accounts, search, content, email, B2B, and AI agent integration. Your team focuses on business differentiators instead of rebuilding standard commerce plumbing. The architecture is designed to be extended and customized, not replaced.
## What happens if I don’t use Evolve?
[Section titled “What happens if I don’t use Evolve?”](#what-happens-if-i-dont-use-evolve)
You would assemble the same pieces yourself: a Next.js frontend, GraphQL API layer, commercetools integration, payment providers, CMS connectors, email delivery, caching, observability, AI agent tooling, multi-cloud Terraform, CI/CD pipelines, and so on. Evolve bundles these into a cohesive, tested, and maintained platform with consistent patterns across all services.
## What technology does Evolve use?
[Section titled “What technology does Evolve use?”](#what-technology-does-evolve-use)
| Layer | Technology |
| --------------- | -------------------------------------------------------------- |
| Frontend | Next.js 16, React 19, Tailwind CSS 4, TypeScript |
| API | GraphQL Federation (Hive Router, Rust), GraphQL Yoga subgraphs |
| Backend | Node.js microservices, Fastify |
| Commerce engine | commercetools |
| CMS | Contentful, Storyblok |
| Search | Algolia, commercetools Search |
| Payments | Adyen, Stripe, Buckaroo, PayNL, invoice |
| Infrastructure | Terraform, Docker, MACH Composer |
| Cloud | AWS, GCP, Azure |
| AI agents | MCP, GraphQL tooling |
| Observability | OpenTelemetry, Sentry, Honeycomb |
For the complete list of supported vendors and their integration status, see [MACH integrations](/architecture/integrations/).
## Is Evolve a replacement for MACH Composer?
[Section titled “Is Evolve a replacement for MACH Composer?”](#is-evolve-a-replacement-for-mach-composer)
No. [MACH Composer](/architecture/cloud/#mach-composer) is an orchestration tool that provisions and wires together cloud infrastructure for MACH-based architectures. Evolve uses MACH Composer as part of its deployment pipeline to manage services, routing, and cloud resources. They are complementary: MACH Composer handles the infrastructure layer, Evolve provides the commerce application that runs on top of it.
## Can I deploy Evolve on Kubernetes?
[Section titled “Can I deploy Evolve on Kubernetes?”](#can-i-deploy-evolve-on-kubernetes)
Not out of the box. Evolve’s Terraform modules target managed container services (AWS ECS Fargate, GCP Cloud Run, Azure Container Apps). Since every service is packaged as a standard Docker container, running on Kubernetes is technically possible, but you would need to write your own Helm charts or Kubernetes manifests. Kubernetes is on the [integration roadmap](/architecture/integrations/).
## Can I deploy Evolve on Vercel?
[Section titled “Can I deploy Evolve on Vercel?”](#can-i-deploy-evolve-on-vercel)
Yes. Evolve can run its full stack on Vercel: the storefront, the GraphQL gateway, and the domain services, as one project. See [Hosting on Vercel](/architecture/cloud/vercel/) for the architecture and [Deploying to Vercel](/how-to/deploying-to-vercel/) for the steps.
## Does Evolve support SaaS system X?
[Section titled “Does Evolve support SaaS system X?”](#does-evolve-support-saas-system-x)
Check the [MACH integrations](/architecture/integrations/) page for the full list of supported providers and their status (Full, Beta, or Roadmap). Evolve currently integrates with 40+ vendors across commerce engines, CMS, PIM, search, payments, shipping, analytics, and more.
If your system is not listed, Evolve’s architecture makes it straightforward to add new integrations. Each integration is a standalone service or package. See the how-to guides for [adding a CMS](/how-to/adding-a-cms/), [adding a search engine](/how-to/adding-a-search-engine/), or [adding a payment provider](/how-to/adding-a-payment-provider/).
## Can I use a different commerce engine than commercetools?
[Section titled “Can I use a different commerce engine than commercetools?”](#can-i-use-a-different-commerce-engine-than-commercetools)
Yes. Evolve’s domain services define their own data model and GraphQL schema. The commercetools integration lives in vendor-specific packages, not in the platform core. Salesforce Commerce Cloud is already supported, and the architecture allows swapping or combining commerce engines without affecting the frontend or other services.
## Does Evolve support B2B?
[Section titled “Does Evolve support B2B?”](#does-evolve-support-b2b)
Yes. Evolve runs B2C and B2B from the same backend. B2B features include business units, associate roles, approval workflows, company account management, and quote requests. A single project can be B2C-only, B2B-only, or both. See [B2B architecture](/architecture/b2b/) for details.
## Is Evolve multi-tenant?
[Section titled “Is Evolve multi-tenant?”](#is-evolve-multi-tenant)
Evolve supports multi-store, multi-language, multi-currency, and multi-brand from a single deployment through its [store context](/architecture/core/store-context/) system. Each store can have its own catalog scope, pricing, locale, and content. For true multi-tenant isolation, [MACH Composer](/architecture/cloud/#mach-composer) can spin up separate Evolve instances per tenant.
## What’s the difference between Evolve and a regular Next.js + commercetools setup?
[Section titled “What’s the difference between Evolve and a regular Next.js + commercetools setup?”](#whats-the-difference-between-evolve-and-a-regular-nextjs--commercetools-setup)
A bare Next.js + commercetools project gives you a frontend framework and a commerce API. Everything in between (GraphQL Federation, authentication and token management, payment orchestration, email notifications, CMS integration, search abstraction, caching, B2B support, AI agent tooling, multi-cloud Terraform, CI/CD, observability) you build yourself. Evolve provides all of those as production-tested, consistent services that work together out of the box, while still letting you customize or replace any part. See the [architecture overview](/architecture/) for how the pieces fit together.
## What Node and pnpm versions does Evolve require?
[Section titled “What Node and pnpm versions does Evolve require?”](#what-node-and-pnpm-versions-does-evolve-require)
Evolve requires **Node.js >= 24.8** and **pnpm >= 10.16.1**. The exact pnpm version (currently `pnpm@11.4.0`) is pinned via the `packageManager` field in `package.json`, so Corepack will install the correct version automatically. See [Getting started](/evolve/getting-started/) for the full setup instructions.
## How do I upgrade Evolve?
[Section titled “How do I upgrade Evolve?”](#how-do-i-upgrade-evolve)
Evolve is distributed as a Git repository that you fork for your project. To pull in updates, merge from the upstream Evolve remote into your project branch. Because Evolve follows consistent patterns across all services, most upgrades are straightforward merges. For breaking changes, release notes in the [changelog](/changelog/) describe what to update.
Evolve’s core logic is published as versioned [framework packages](/architecture/core/framework/) (`@evolve-framework/*`), pinned through the pnpm catalog in `pnpm-workspace.yaml`. Updating the framework is a catalog version bump — a dependency update rather than a Git merge.
## How is Evolve licensed?
[Section titled “How is Evolve licensed?”](#how-is-evolve-licensed)
Evolve is proprietary software by Lab Digital BV. The source code is available under the **Evolve Platform Review License**, which allows you to review and evaluate the platform for internal assessment purposes. Any commercial use, modification, or deployment requires a valid Evolve Platform license agreement with Lab Digital. Contact Lab Digital for licensing terms.
## How do I get started?
[Section titled “How do I get started?”](#how-do-i-get-started)
See [Creating a new Evolve project](/how-to/creating-a-project/) for the step-by-step guide on forking the platform and setting up your project repository.
# How-to guides
> Practical guides for common tasks when working with Evolve.
Step-by-step guides for common development tasks.
### Getting started
[Section titled “Getting started”](#getting-started)
* [Creating a new Evolve project](/how-to/creating-a-project/) - Fork the platform and set up a clean project repository
* [Contributing](/how-to/contributing/) - Development workflow and submission checklist for your project
### Backend
[Section titled “Backend”](#backend)
* [Creating a new domain service](/how-to/creating-a-service/) - The four-file pattern (config, module, server, entry point), framework modules, and supergraph composition
* [Adding a new CMS](/how-to/adding-a-cms/) - Integrate a new CMS end to end, with Sanity as a worked example
* [Adding a search engine](/how-to/adding-a-search-engine/) - Create an implementation package for a different search engine
* [Integrating an external pricing engine](/how-to/external-pricing-engine/) - Add an external price source as a federated subgraph
* [Adding a payment provider](/how-to/adding-a-payment-provider/) - Framework package with payment adapter and module, mounted by a thin platform service
* [Adding a custom event and handler](/how-to/custom-events/) - Zod schemas, publishing, cloud handlers, and local testing
* [Customizing email templates](/how-to/customizing-email-templates/) - Add a new transactional email notification end to end
* [Configuring Datadog with OpenTelemetry](/how-to/datadog-opentelemetry/) - OTel Collector exporter, API keys, and verification
### Frontend
[Section titled “Frontend”](#frontend)
* [Adding a field to the checkout](/how-to/checkout-custom-field/) - Schema, form component, Zod validation, and cart mutation
* [Adding a language to the storefront](/how-to/adding-a-language/) - next-intl, translations, store config, and CMS localization
* [Adding a custom facet component](/how-to/adding-a-facet-component/) - Custom visual facet variant (e.g. color swatches) for product listing filters
* [Adding a CMS content block](/how-to/adding-a-cms-block/) - New block type end to end: schema, mapper, and React component
### Developer experience
[Section titled “Developer experience”](#developer-experience)
* [Local HTTPS with Portless](/how-to/local-https-portless/) - Stable `*.evolve.localhost` hostnames with system-trusted certs for OAuth callbacks, secure cookies, and cross-subdomain editor sessions
### Operations
[Section titled “Operations”](#operations)
* [Optimizing deployments](/how-to/optimizing-deployments/) - Terraform artifact bundling for faster CI/CD
# Adding a new CMS
> How to add a new CMS integration to the Evolve platform, using Sanity as a worked example.
Evolve’s CMS layer is designed as a pluggable adapter: every CMS integration implements the same GraphQL schema, so the frontend and gateway don’t need to change when you swap or add a CMS. This guide walks through building a new CMS integration from scratch, using **Sanity** as the example. (No Sanity implementation ships today — Contentful and Storyblok are the two existing integrations.)
## How the CMS layer works
[Section titled “How the CMS layer works”](#how-the-cms-layer-works)
All CMS logic lives in the framework repository: `@evolve-framework/contentful` and `@evolve-framework/storyblok` each own the shared `schema.graphql`, the mappers, resolvers, loaders, webhook handler, and a Module class that wires it all together. The schema defines page types (`ContentPage`, `CatalogPage`), a `Block` union for content blocks, site layout types (`SiteHeader`, `SiteFooter`, `NavigationMenu`), and federation extensions for product breadcrumbs.
The platform side is a thin mount: `backend/services/cms-storyblok` and `backend/services/cms-contentful` contain only `config.ts`, `module.ts`, `server.ts`, and `index.ts`. The package acts as a translation layer:
```plaintext
Sanity API → mappers → shared GraphQL types → frontend components
```
The frontend never sees CMS-specific data. It queries the same `page`, `siteLayout`, and `contentSnippet` fields regardless of which CMS package is behind the gateway.
Adding a CMS therefore means three things: a new `@evolve-framework/` package exposing a Module, a thin platform service mounting it, and the `cms` supergraph slot pointing at its schema.
## 1. Scaffold the framework package
[Section titled “1. Scaffold the framework package”](#1-scaffold-the-framework-package)
Create the package in the framework repository, mirroring `packages/storyblok`:
* packages/sanity/
* src/
* index.ts Public exports (module, webhook, utils)
* modules/
* sanity.ts SanityModule (AbstractModule)
* webhook.ts Webhook handler factory
* graphql/
* typedefs.ts Generated from schema.graphql
* types.ts Generated resolver/entity types
* context.ts ContextValue + initContextValue
* lib/
* clients.ts Sanity client setup (module-scope singletons)
* cache.ts
* resolvers/
* index.ts
* query/ page, pages, site-layout, content-snippet
* …
* site-layout/
* …
* mutations/
* store-content-preview\.ts
* mappers/
* pages.ts Sanity document → ContentPage/CatalogPage
* map-blocks.ts Block array mapper
* map-asset.ts Sanity image → ContentAsset
* map-resource-link.ts
* loaders/
* snippets-by-slug-loader.ts
* events/
* internal/
* handler.ts
* testing/ Mocks, test server, context factory
* …
* codegen.ts
* schema.graphql Copied from packages/storyblok (shared contract)
* package.json
The `schema.graphql` is copied verbatim from `packages/storyblok/schema.graphql` (or `packages/contentful/schema.graphql` — they define the same contract). This is what makes CMS integrations interchangeable.
## 2. Generate typedefs and types
[Section titled “2. Generate typedefs and types”](#2-generate-typedefs-and-types)
The package’s `codegen.ts` merges the local schema with the shared `storeContextSnippet` and `errorsSnippet` from `@evolve-framework/schemas`, then generates `src/graphql/typedefs.ts` (via `@labdigital/graphql-codegen-typedefs`) and `src/graphql/types.ts` (resolver types). Copy `packages/storyblok/codegen.ts` as a starting point and run `pnpm codegen` in the package.
For the `RichText` block type, the schema has a `renderer` enum that currently supports `contentful` and `storyblok`. Add `sanity` to it in your copy of the schema:
```graphql
enum RichTextRenderer {
contentful
storyblok
sanity
}
```
Serialize Sanity’s Portable Text as JSON, just as Storyblok serializes its rich text, and set `renderer: "sanity"` so the frontend picks the right renderer component.
## 3. Configure the Sanity clients
[Section titled “3. Configure the Sanity clients”](#3-configure-the-sanity-clients)
Follow the pattern in `packages/storyblok/src/lib/clients.ts`: a config type, a `createClients` factory, and module-scope singleton accessors that the Module populates in `init()`:
src/lib/clients.ts
```typescript
import { createClient, type SanityClient } from "@sanity/client";
export type SanityConfig = {
projectId: string;
dataset: string;
/** Read token for published content. */
apiToken: string;
/** Token that can read draft content. */
previewToken: string;
apiVersion?: string;
};
export type SanityClients = {
client: SanityClient;
previewClient: SanityClient;
};
export const createSanityClients = (config: SanityConfig): SanityClients => {
const shared = {
projectId: config.projectId,
dataset: config.dataset,
apiVersion: config.apiVersion ?? "2025-01-01",
};
return {
client: createClient({ ...shared, token: config.apiToken, useCdn: true }),
previewClient: createClient({
...shared,
token: config.previewToken,
useCdn: false,
perspective: "drafts",
}),
};
};
let clients: SanityClients | undefined;
export const setSanityClients = (value: SanityClients): void => {
clients = value;
};
export const getSanityClients = (): SanityClients => {
if (!clients) {
throw new Error("Sanity clients not configured. Construct a SanityModule first.");
}
return clients;
};
```
Two clients: a CDN-backed client for production reads and a preview client that returns draft content. This mirrors the Storyblok access/management client split and the Contentful delivery/preview client split.
## 4. Create the mappers
[Section titled “4. Create the mappers”](#4-create-the-mappers)
This is the core of the CMS adapter. Sanity documents use `_type` as the discriminator (vs. Storyblok’s `component` field and Contentful’s `sys.contentType`). The block mapper dispatches on `_type` and produces `__typename`-tagged objects matching the shared GraphQL schema — see `packages/storyblok/src/mappers/map-bloks.ts` for the reference switch. Each mapper function sets `__typename` explicitly; this is how the frontend resolves the `Block` union:
```typescript
const mapTeaserBlock = (
data: SanityContentTeaser,
storeContext: StoreContext,
): TeaserBlock => ({
__typename: "TeaserBlock",
id: data._key,
title: data.title,
content: data.content,
link: mapResourceLink(data.link),
image: data.image ? mapSanityImage(data.image) : undefined,
imagePosition: data.imagePosition ?? "left",
});
```
Note that Sanity uses `_key` for array items (vs. Storyblok’s `_uid`).
Mapper registry
`@evolve-framework/contentful` goes one step further: its default mappers are installed into a `MapperRegistry` (`src/mappers/registry.ts`), and the `ContentfulModule` accepts a `mappers` callback so implementations can add or override mappers for custom content types without forking the package. Consider the same design for a new CMS package.
## 5. Implement the resolvers
[Section titled “5. Implement the resolvers”](#5-implement-the-resolvers)
Implement the same resolver surface both existing packages provide — compare `packages/storyblok/src/resolvers/index.ts`:
src/resolvers/index.ts
```typescript
import type { Resolvers } from "#src/graphql/types.ts";
export const resolvers: Resolvers = {
Mutation: {
storeContentPreview: storeContentPreviewMutation,
},
Query: {
page: pageResolver(undefined),
pages: pagesResolver,
catalogPage: pageResolver("catalogPage"),
contentPage: pageResolver("contentPage"),
siteLayout: siteLayoutResolver,
contentSnippet: contentSnippetResolver,
},
SiteLayout: siteLayoutFieldResolvers,
};
```
The query resolvers use GROQ to fetch documents and hand them to the mappers. Use DataLoaders (see `packages/storyblok/src/loaders/`) for anything fetched per-field.
## 6. Write the Module
[Section titled “6. Write the Module”](#6-write-the-module)
The Module is what the platform service mounts. It extends `AbstractModule` from `@evolve-framework/core`: the generated `typedefs` and the `resolvers` map are assigned as fields (which makes `getGraphQLConfig()` and `getGraphQLSchema()` work), `init()` builds the clients, and `getHttpConfig()` registers the webhook route:
src/modules/sanity.ts
```typescript
import { AbstractModule, type HttpConfig } from "@evolve-framework/core";
import type { MessagePublisher } from "@evolve-framework/core/messaging";
import { typeDefs } from "#src/graphql/typedefs.ts";
import {
createSanityClients,
type SanityConfig,
setSanityClients,
} from "#src/lib/clients.ts";
import { resolvers } from "#src/resolvers/index.ts";
import { createWebhookHandler } from "#src/webhook.ts";
export type SanityModuleOptions = {
config: SanityConfig;
/** Publisher used to emit `content-modified` events from the webhook. */
messagePublisher: MessagePublisher;
/** URL path the webhook is registered on. Defaults to "/api/webhook". */
webhookPath?: string;
};
export class SanityModule extends AbstractModule {
private readonly options: SanityModuleOptions;
resolvers = resolvers;
typedefs = typeDefs;
constructor(options: SanityModuleOptions) {
super();
this.options = options;
}
override init(): void {
setSanityClients(createSanityClients(this.options.config));
}
override getHttpConfig(): HttpConfig {
const handler = createWebhookHandler({
messagePublisher: this.options.messagePublisher,
});
return {
routes: (app) => {
app.route({
url: this.options.webhookPath ?? "/api/webhook",
method: ["GET", "POST"],
handler,
});
},
};
}
}
```
The webhook handler receives Sanity’s change notifications, flushes the cache, and publishes a `content-modified` event through the injected `MessagePublisher` — same as `packages/storyblok/src/webhook.ts`.
Keep the constructor cheap and side-effect free: platform codegen instantiates the module and calls `getGraphQLSchema()` without runtime config or network access.
Publish the package to your own registry (or keep it as a workspace package in your project’s monorepo) and pin it in your `catalog:` like the other framework packages. If the integration is broadly useful, consider submitting it upstream so it can ship as an official `@evolve-framework/*` package.
## 7. Create the thin platform service
[Section titled “7. Create the thin platform service”](#7-create-the-thin-platform-service)
Create `backend/services/cms-sanity/` following `backend/services/cms-storyblok`. Four source files:
src/module.ts
```typescript
import { SanityModule } from "@evolve-framework/sanity";
import { resolvePublisher } from "@evolve-packages/cloud-adapters";
import { config } from "./config.ts";
export const createModule = (): SanityModule => {
return new SanityModule({
config: {
projectId: config.SANITY_PROJECT_ID,
dataset: config.SANITY_DATASET,
apiToken: config.SANITY_API_TOKEN,
previewToken: config.SANITY_PREVIEW_TOKEN,
},
messagePublisher: resolvePublisher(config.INTERNAL_EVENTS_TARGET),
});
};
```
src/server.ts
```typescript
import { createDomainService } from "@evolve-framework/core";
import { cache } from "@evolve-framework/core/cache";
import {
type ContextValue,
initContextValue,
} from "@evolve-framework/sanity/graphql/context";
import { config, loadConfig } from "#src/config.ts";
import { createModule } from "./module.ts";
export type { ContextValue };
export const startServer = async (): Promise => {
await loadConfig();
await cache.configure(config.REDIS_URL);
const service = await createDomainService({
name: config.COMPONENT_NAME,
module: createModule(),
graphql: {
context: initContextValue,
},
http: {
address: {
host: config.HTTP_HOST,
port: config.HTTP_PORT,
},
},
});
await service.start();
};
```
`config.ts` extends `BaseConfig` from `@labdigital/enviconf` with `COMPONENT_NAME = "cms"`, `HTTP_PORT = 4004`, and the Sanity variables; `index.ts` just imports and awaits `startServer()`. Add `@evolve-framework/sanity` to the service’s `package.json` with `catalog:` as the version and pin the version in `pnpm-workspace.yaml`.
The service’s `codegen.ts` generates `schema.generated.graphql` directly from the module — no schema copy in the platform repo:
```typescript
// codegen.ts (excerpt)
import { createModule } from "./src/module.ts";
const config: CodegenConfig = {
schema: createModule().getGraphQLSchema(),
generates: {
"schema.generated.graphql": { plugins: ["schema-ast"] },
},
};
```
Composite modules
`cms-contentful` mounts a `CompositeModule` combining the delivery `ContentfulModule` with the `ContentfulAuthoringModule`. If your CMS grows an authoring surface, compose it the same way instead of adding a second service.
## 8. Register the supergraph slot
[Section titled “8. Register the supergraph slot”](#8-register-the-supergraph-slot)
The gateway composes one `cms` subgraph slot, and which CMS fills it is a compose-time choice. The Taskfile in `backend/services/graphql-gateway` points the slot at `../cms-{{.CMS}}/schema.generated.graphql`, so once your service directory follows the `cms-` convention it works out of the box:
```bash
task -d backend/services/graphql-gateway supergraph CMS=sanity
```
The default is `contentful`; `pnpm dev:backend` picks up whatever `CMS` is set to. All CMS services listen on port 4004, so only the schema file differs between compositions.
## 9. Add Terraform
[Section titled “9. Add Terraform”](#9-add-terraform)
Follow the [tri-cloud pattern from “Creating a service”](/how-to/creating-a-service/#6-add-terraform) and the existing `backend/services/cms-storyblok/terraform/` layout. Inject the Sanity tokens as secrets through Key Vault (Azure), Secrets Manager (AWS), or Secret Manager (GCP), never as plain environment variables.
Sanity webhooks are configured through the Sanity management API or the project dashboard (there is no first-party Terraform resource, unlike Storyblok’s `storyblok_webhook` on AWS). Point the webhook at the service’s `/api/webhook` endpoint.
Register the component in Mach Composer following the [registration pattern](/how-to/creating-a-service/#register-in-mach-composer). The `integrations` list does **not** include `commercetools` — the CMS service doesn’t interact with commercetools directly.
## 10. Frontend: rich text and preview
[Section titled “10. Frontend: rich text and preview”](#10-frontend-rich-text-and-preview)
Two frontend pieces are CMS-specific; everything else works unchanged because all block components consume the shared GraphQL types.
**Rich text**: `frontend/site/src/components/content-blocks/rich-text.tsx` switches on the `renderer` field. Add a `sanity` case that renders Portable Text via `@portabletext/react`:
```tsx
case "sanity":
return ;
```
**Preview package**: each CMS has a frontend package under `frontend/packages/` (`contentful-preview`, `storyblok-preview`) enabling live editing in the CMS’s visual editor. They share a four-export structure:
| Export | Purpose |
| ------------- | -------------------------------------------------------------------- |
| `./api-route` | Next.js route handler that validates a secret and enables draft mode |
| `./component` | Returns data attributes for in-page click-to-edit |
| `./provider` | Client component that initializes the live preview bridge |
| `./server` | Server utilities for preview cookies and draft mode |
Create `@evolve-storefront/sanity-preview` with the same exports (use `@sanity/visual-editing` for the provider), then register the new cases in the site’s CMS abstraction layer, which switches on the `EVOLVE_CMS` environment variable:
* `frontend/site/src/lib/cms-preview.ts` — add `sanity` cases to the `cmsPreviewAttributes` and `getContentPreviewID` switches
* `frontend/site/src/lib/cms-preview.client.tsx` — add the provider case
Set `EVOLVE_CMS=sanity` in the frontend environment to activate it.
## Checklist
[Section titled “Checklist”](#checklist)
* [ ] Framework package `@evolve-framework/sanity` created, mirroring `packages/storyblok` (schema, codegen, clients, mappers, resolvers, loaders, webhook, testing helpers)
* [ ] `schema.graphql` copied from an existing CMS package (add `sanity` to `RichTextRenderer` enum)
* [ ] `SanityModule` extends `AbstractModule`, assigns `resolvers`/`typedefs`, builds clients in `init()`, registers the webhook in `getHttpConfig()`
* [ ] Package published (or workspace-linked) and pinned in the `catalog:` `pnpm-workspace.yaml` catalog
* [ ] Thin platform service `backend/services/cms-sanity` with `config.ts` + `module.ts` + `server.ts` + `index.ts`, codegen producing `schema.generated.graphql`
* [ ] Supergraph composes with `task supergraph CMS=sanity`
* [ ] Terraform deploys the service; webhook registered in Sanity
* [ ] Frontend: Portable Text renderer case, preview package, `cms-preview` abstraction updated, `EVOLVE_CMS=sanity` set
* [ ] Run `pnpm codegen` after schema changes and `pnpm check` to verify
## Further reading
[Section titled “Further reading”](#further-reading)
* [Creating a new domain service](/how-to/creating-a-service/) for the full service scaffolding pattern and Terraform setup
* [Adding a CMS content block](/how-to/adding-a-cms-block/) for adding new block types to an existing CMS integration
* [Customization](/architecture/core/framework/customization/) for extending the schema with project-specific types
# Adding a CMS content block
> How to add a project-specific CMS content block end to end - schema extension, mapper, and React component - without modifying the framework.
Content blocks are the building blocks of CMS pages. Each block type follows a pipeline: CMS content type → mapper → GraphQL type → React component. This guide walks through adding a project-specific block (a `ProjectTeaser`) end to end.
For Contentful, the whole pipeline is extensible from your project: the `Block` union is open for extension via a project-owned module, and `ContentfulModule` accepts a `mappers` callback for wiring your content types. No framework changes are needed. Contributing a block upstream to the framework packages is the secondary path, covered at the end — as is the current state of Storyblok, which does not yet have the same extension hooks.
## 1. Define the block in a project-owned module
[Section titled “1. Define the block in a project-owned module”](#1-define-the-block-in-a-project-owned-module)
Create a module in the CMS service that owns the new GraphQL type and extends the framework’s `Block` union. First add `graphql-tag` to the service (it is already in the workspace `catalog:`):
```json
// backend/services/cms-contentful/package.json — dependencies
"graphql-tag": "catalog:"
```
Then define the module:
backend/services/cms-contentful/src/project-blocks.ts
```typescript
import type { MapperRegistry } from "@evolve-framework/contentful";
import type { Block } from "@evolve-framework/contentful/graphql/types";
import { AbstractModule } from "@evolve-framework/core";
import type { Entry, EntrySkeletonType } from "contentful";
import { gql } from "graphql-tag";
export class ProjectBlocksModule extends AbstractModule {
override typedefs = gql`
extend union Block = ProjectTeaser
type ProjectTeaser {
id: ID!
title: String!
content: String
}
`;
override resolvers = {};
}
```
The `extend union` is merged into the framework’s union when the modules are composed (next step). `resolvers` stays empty: the union is resolved through the `__typename` your mapper sets, so a plain type with scalar fields needs no resolver of its own.
## 2. Compose it in `src/module.ts`
[Section titled “2. Compose it in src/module.ts”](#2-compose-it-in-srcmodulets)
Add the module to the service’s `CompositeModule`:
backend/services/cms-contentful/src/module.ts
```typescript
import { ContentfulModule } from "@evolve-framework/contentful";
import { CompositeModule } from "@evolve-framework/core";
import { resolvePublisher } from "@evolve-packages/cloud-adapters";
import { config } from "./config.ts";
import { ProjectBlocksModule, registerProjectMappers } from "./project-blocks.ts";
export const createModule = (): CompositeModule => {
return new CompositeModule([
new ContentfulModule({
config: {
spaceId: config.CONTENTFUL_SPACE_ID,
environment: config.CONTENTFUL_ENVIRONMENT,
accessToken: config.CONTENTFUL_ACCESS_TOKEN,
previewToken: config.CONTENTFUL_PREVIEW_TOKEN,
managementToken: config.CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN,
},
messagePublisher: resolvePublisher(config.INTERNAL_EVENTS_TARGET),
mappers: registerProjectMappers,
}),
new ProjectBlocksModule(),
]);
};
```
`CompositeModule` merges the typedefs of all its modules into one subgraph schema. The resulting union reads:
```graphql
union Block = CatalogBlock | HeroBlock | OneColumnBlock | ProductBlock | TeaserBlock | TeasersBlock | ThreeColumnsBlock | TwoColumnsBlock | UspBlock | ProjectTeaser
```
## 3. Register the content-type mapper
[Section titled “3. Register the content-type mapper”](#3-register-the-content-type-mapper)
The `mappers` callback passed above receives the `MapperRegistry`, pre-populated with the framework defaults. Mappers are keyed by the Contentful `sys.contentType.sys.id`; registering an already-known id overrides the default mapper without throwing.
```typescript
// backend/services/cms-contentful/src/project-blocks.ts (continued)
type ProjectTeaserSkeleton = EntrySkeletonType<
{ title: string; content?: string },
"projectTeaser"
>;
export const registerProjectMappers = (registry: MapperRegistry): void => {
registry.registerBlock("projectTeaser", (_context, entry) => {
const data = entry as Entry<
ProjectTeaserSkeleton,
"WITHOUT_UNRESOLVABLE_LINKS",
string
>;
return {
__typename: "ProjectTeaser",
id: data.sys.id,
title: data.fields.title,
content: data.fields.content,
} as unknown as Block;
});
};
```
Two casts are deliberate here. The registry hands you a generic `BaseEntry`, so narrow it to your content type’s skeleton (the framework’s own mappers do the same). And the framework’s generated `Block` TypeScript union does not know about project types, so the return value is cast through `unknown` — the GraphQL layer resolves the union via the explicit `__typename`.
Create the matching `projectTeaser` content type in Contentful and add it to the page body field’s validations so editors can place it. You can regenerate entry types from the content model with `pnpm codegen:contentful` instead of hand-writing the skeleton.
## 4. Regenerate the schema and supergraph
[Section titled “4. Regenerate the schema and supergraph”](#4-regenerate-the-schema-and-supergraph)
The service’s schema file is generated from the composed module, so the new type flows through automatically:
```bash
pnpm --filter @evolve-platform/cms-contentful codegen # schema.generated.graphql
pnpm --filter @evolve-platform/graphql-gateway supergraph
```
Check `backend/services/cms-contentful/schema.generated.graphql`: the `Block` union should now include `ProjectTeaser`. The supergraph recomposition makes the type available to the gateway and, from there, the frontend. A root-level `pnpm codegen` runs both plus the frontend types in one go.
## 5. Create the React component
[Section titled “5. Create the React component”](#5-create-the-react-component)
Create the frontend component with a co-located GraphQL fragment, following the pattern of existing blocks like `teaser-block.tsx`:
frontend/site/src/components/content-blocks/project-teaser.tsx
```tsx
import { cn } from "@evolve-storefront/ui/helpers/styles.ts";
import type { ResultOf } from "@graphql-typed-document-node/core";
import type { ReactNode } from "react";
import { graphql } from "#generated/gql.ts";
import { cmsPreviewAttributes } from "#lib/cms-preview.ts";
export const ProjectTeaserFragment = graphql(/* GraphQL */ `
fragment ProjectTeaserFragment on ProjectTeaser {
__typename
id
title
content
}
`);
type Props = {
className?: string;
data: ResultOf;
};
export const ProjectTeaser = ({ className, data }: Props): ReactNode => {
const { title, content } = data;
return (
{title}
{content &&
{content}
}
);
};
```
Key patterns:
* **Fragment co-location**: the fragment lives in the same file as the component, keeping data requirements explicit.
* **`cmsPreviewAttributes`**: enables the CMS’s visual editor to highlight this block for in-place editing.
* **`#generated/gql.ts`**: the `graphql` function from codegen provides type-safe fragment definitions.
## 6. Register in the block renderer
[Section titled “6. Register in the block renderer”](#6-register-in-the-block-renderer)
Open `render-block.tsx` and wire the new component:
frontend/site/src/components/content-blocks/render-block.tsx
```tsx
// 1. Import the component and fragment
import {
ProjectTeaser,
ProjectTeaserFragment,
} from "./project-teaser.tsx";
// 2. Add the fragment spread to RenderBlockFragment
export const RenderBlockFragment = graphql(/* GraphQL */ `
fragment RenderBlockFragment on Block {
...OneColumnBlockFragment
...TwoColumnBlockFragment
...ThreeColumnsBlockFragment
...ProductBlockFragment
...UspBlockFragment
...TeaserBlockFragment
...TeasersBlockFragment
...HeroBlockFragment
...CatalogBlockFragment
...ProjectTeaserFragment
}
`);
// 3. Add the switch case
case "ProjectTeaser":
return (
);
```
Most blocks are wrapped in `` for consistent page layout. Only full-width blocks like `HeroBlock` skip the wrapper.
## 7. Run codegen and verify
[Section titled “7. Run codegen and verify”](#7-run-codegen-and-verify)
After all changes, regenerate the TypeScript types and check for errors:
```bash
pnpm codegen
pnpm check
```
Create a test page in Contentful with the new content type to verify the full pipeline.
## Storyblok
[Section titled “Storyblok”](#storyblok)
The Storyblok package does not yet offer the same extension hooks: `StoryblokModule` has no `mappers` option and no registry — block mapping is a hardcoded `switch` on `block.component` in `packages/storyblok/src/mappers/map-bloks.ts`. Extending the `Block` union through a project-owned module composed alongside `StoryblokModule` works the same as for Contentful, but there is currently no way to plug in the mapper from your project. Until such a hook exists, project-specific Storyblok blocks go through the framework-contribution route below.
## Contributing a block upstream
[Section titled “Contributing a block upstream”](#contributing-a-block-upstream)
When a block is generic enough to benefit every Evolve project, move it into the framework instead of keeping it project-owned: add the type to the shared `schema.graphql` of **both** `@evolve-framework/contentful` and `@evolve-framework/storyblok` (the schema is a shared contract, so the two stay interchangeable), plus the default mapper in each package. Consuming the update is a package bump:
1. Bump the `@evolve-framework/contentful` (and `@evolve-framework/storyblok`) version in the `catalog:` section of your `pnpm-workspace.yaml`, then run `pnpm install`.
2. Run `pnpm codegen` — the CMS service regenerates its `schema.generated.graphql` from the module, and the gateway supergraph recomposes from it.
3. Delete your project-owned typedefs and mapper for the block, keeping the frontend component.
## Further reading
[Section titled “Further reading”](#further-reading)
* [Adding a new CMS](/how-to/adding-a-cms/) for the full framework-package layout
* [Customization](/architecture/core/framework/customization/) for extending the schema with project-specific types
# Adding a custom facet component
> How to add a visual facet variant (such as color swatches) to the product listing filters.
The product listing page renders facets generically: checkboxes for options, sliders for ranges. Sometimes you need a richer UI for a specific facet, such as color swatches instead of text labels. This guide walks through adding a color-swatch facet for an `OptionsFacet` with key `"color"`.
The key insight: you don’t need a new GraphQL facet type. The backend already exposes options facets generically. You customise the **rendering** of an existing `OptionsFacet` based on its `key`.
1. **Identify the facet key**
Check your facet configuration to confirm the key used for colors. The facet data coming from the backend looks like this:
```graphql
fragment OptionsFacetFragment on OptionsFacet {
__typename
key # e.g. "color"
label # e.g. "Color"
options {
key # e.g. "red", "blue"
label # e.g. "Red", "Blue"
count
selected
}
}
```
The option `key` values (e.g. `"red"`, `"navy-blue"`) are what you’ll map to hex codes.
2. **Create the color swatch component**
Create a new file following the pattern of the existing `OptionsFacetDesktop`:
```plaintext
frontend/site/src/app/[locale]/(main)/(product-listing)/
_components/filters/color-swatch-facet.tsx
```
The component reuses the same `OptionsFacetFragment`, so no new fragment is needed:
```tsx
import { cn } from "@evolve-storefront/ui/helpers/styles.ts";
import type { ResultOf } from "@graphql-typed-document-node/core";
import { type ReactNode, useEffect, useState } from "react";
import type { OptionsFacetFragment } from "./options-facet-desktop.tsx";
const COLOR_MAP: Record = {
red: "#DC2626",
blue: "#2563EB",
green: "#16A34A",
black: "#171717",
white: "#FAFAFA",
"navy-blue": "#1E3A5F",
// Add more mappings as needed
};
type Props = {
data: ResultOf;
className?: string;
onChange: (value: string, selected: boolean) => void;
isPending: boolean;
};
export const ColorSwatchFacet = ({
data,
className,
onChange,
isPending,
}: Props): ReactNode => {
const [clickedKey, setClickedKey] = useState(null);
useEffect(() => {
if (!isPending) setClickedKey(null);
}, [isPending]);
return (
);
};
```
Key decisions:
* **Same fragment, different UI**: the component uses `OptionsFacetFragment` from `options-facet-desktop.tsx`, keeping GraphQL data requirements unchanged.
* **`COLOR_MAP` lookup**: maps option keys to hex values. For a more dynamic approach, you could store hex codes as attribute metadata in commercetools and expose them through the `OptionsFacet` options.
* **Selected state**: a thicker border and slight scale increase indicate the active swatch, matching the checkbox behavior of the default component.
3. **Route the facet in `facet-input.tsx`**
Open `facet-input.tsx` and add a case for the `"color"` key, following the same pattern as the existing `"brand"` routing:
```tsx
import { ColorSwatchFacet } from "./color-swatch-facet.tsx";
// Inside the OptionsFacet case:
case "OptionsFacet":
if (facetResult.key === "brand") {
return (
);
} else if (facetResult.key === "color") {
return (
{
const url = new URL(window.location.href);
if (selected) {
url.searchParams.append(facetResult.key, value);
} else {
url.searchParams.delete(facetResult.key, value);
}
startTransition(() => {
onChange(url);
router.push(url.toString());
});
}}
/>
);
} else {
return (
);
}
```
The URL mutation logic is identical to the other options facets: append or delete the search parameter based on the selection state.
4. **Handle the color-to-hex mapping**
The `COLOR_MAP` approach works for a small, known set of colors. For a more scalable solution, consider:
* **Attribute metadata**: store hex codes as product attribute values in commercetools and expose them in the facet response. This avoids hardcoding and works for any color catalog.
* **CSS custom properties**: if your design system already defines color tokens, map facet keys to token names instead of raw hex values.
5. **Mobile variant**
The mobile filter sheet uses the same `FacetInput` component as desktop, so adding the `"color"` case in `facet-input.tsx` automatically applies to both. No separate mobile component or routing is needed.
If the swatch layout doesn’t work well on smaller screens, you can check the viewport inside `ColorSwatchFacet` and adjust sizing or wrapping accordingly.
## Further reading
[Section titled “Further reading”](#further-reading)
* [Frontend development](/evolve/frontend/) for component patterns and data fetching
* [Architecture overview](/architecture/) for how facets flow from the search service through the gateway to the frontend
# Adding a language to the storefront
> How to add a new language to the Evolve storefront.
Adding a language touches multiple layers: the shared locale configuration, translation files, the store config that sets the locale header, URL prefix mapping, and CMS content. This guide walks through each step.
1. **Update the locale configuration**
The locale list lives in the shared `site-config` package. Add the new locale and its URL prefix:
packages/site-config/src/i18n/config.ts
```typescript
export const locales = [
"nl-NL",
"en-GB",
"de-DE", // new
] as const;
export const defaultLocale = "nl-NL";
export const localePrefixes: LocalePrefixes = {
"nl-NL": "/nl",
"en-GB": "/en",
"de-DE": "/de", // new
};
```
The locale prefix determines the URL path (e.g., `/de/products/...`). Next.js uses next-intl routing to match the prefix to the locale.
2. **Add translation files**
Create a translation file for the new locale:
```plaintext
frontend/site/src/messages/de-DE.json
```
Copy an existing locale file (e.g., `en-GB.json`) as a starting point and translate the values:
```json
{
"common": {
"addToCart": "In den Warenkorb",
"checkout": "Zur Kasse",
"search": "Suchen"
}
}
```
next-intl loads the file automatically based on the locale from the URL.
3. **Update the store configuration**
The store configuration determines which locales are available for each store deployment. Add the new locale to your store config (the shipped configs are the `example-*.ts` files in `frontend/site/src/lib/store-config/`):
frontend/site/src/lib/store-config/my-store.ts
```typescript
export const storeConfig: StoreConfig = {
storeKey: "my-store",
defaultLocale: "en-GB",
locales: ["en-GB", "nl-NL", "de-DE"],
currency: "EUR",
country: "DE",
// ...
};
```
The frontend reads this config to set the `X-StoreContext-Locale` header on every GraphQL request. Backend services use this header to return product names, descriptions, and category names in the correct language.
4. **Add commercetools translations**
Make sure your commercetools project has the locale configured and that product data includes translations for the new language. This is typically managed through the Merchant Center or via the commercetools API.
In Terraform, add the locale to the project settings:
```hcl
resource "commercetools_project_settings" "project" {
languages = ["en-GB", "nl-NL", "de-DE"]
currencies = ["EUR"]
countries = ["GB", "NL", "DE"]
}
```
5. **Localize CMS content**
If you use Contentful or Storyblok, enable the new locale in your CMS space and add translations for content entries. The CMS services in Evolve use the `X-StoreContext-Locale` header to fetch the correct language version of each entry.
For Contentful, add the locale in the space settings (or through Terraform). For Storyblok, the locale is extracted from the header and passed to the Storyblok API as the `language` parameter.
6. **Verify**
Start the development server and navigate to the new locale prefix:
3000/de
```bash
pnpm dev
```
Check that:
* UI labels show the translated strings from `de-DE.json`
* Product names and descriptions appear in German (if translations exist in commercetools)
* CMS content renders in the correct language
## Further reading
[Section titled “Further reading”](#further-reading)
* [Multi-store support](/architecture/core/store-context/) for how StoreContext carries the locale through the entire request lifecycle
* [Frontend development](/evolve/frontend/) for the full project structure and internationalization setup
# Adding a payment provider
> How to add a new payment provider to the Evolve checkout flow.
Each payment provider in Evolve is a framework package that communicates with its PSP and updates commercetools Payment objects. The package is mounted by a thin platform service, and the checkout service orchestrates the flow and reacts to payment state changes through events. This guide walks through creating a new provider, using a fictional PSP called “Acme” as the example.
## 1. Author the framework package
[Section titled “1. Author the framework package”](#1-author-the-framework-package)
Payment providers live in the framework repository as `@evolve-framework/commercetools-` packages. Use `evolve-framework/packages/commercetools-stripe` as the reference implementation:
* packages/commercetools-acme/
* src/
* index.ts Public exports (module, adapter, client)
* module.ts AcmePaymentModule
* adapter.ts AcmePaymentAdapter
* lib/
* client.ts PSP SDK client setup
* core/ PSP-specific transaction and methods logic
* …
* routes/
* healthcheck.ts
* push.ts Webhook handler
* redirect.ts Customer redirect back from the PSP
* package.json
### Implement the payment adapter
[Section titled “Implement the payment adapter”](#implement-the-payment-adapter)
The payment contract lives in `@evolve-framework/commercetools/payments`. It provides the abstract `PaymentAdapter` class with two methods to implement (`createTransaction` and `getPaymentMethods`) and ready-made commercetools helpers (`addPaymentTransaction`, `updatePayment`, `getTargetByPaymentId`, `updateTarget`) that handle Payment updates and state transitions for you:
src/adapter.ts
```typescript
import type {
ByProjectKeyRequestBuilder,
Cart,
Order,
Payment,
} from "@commercetools/platform-sdk";
import {
PaymentAdapter,
type TransactionRequest,
type TransactionResponse,
} from "@evolve-framework/commercetools/payments";
import type { AcmeClient } from "#src/lib/client.ts";
export class AcmePaymentAdapter extends PaymentAdapter {
private readonly acmeApi: AcmeClient;
constructor(ctApi: ByProjectKeyRequestBuilder, acmeApi: AcmeClient) {
super(ctApi);
this.acmeApi = acmeApi;
}
public readonly createTransaction: PaymentAdapter["createTransaction"] =
async (
data: TransactionRequest,
target: Cart | Order,
payment: Payment,
): Promise => {
// Call your PSP SDK to start a transaction
const result = await this.acmeApi.createTransaction({
amount: data.amount.centAmount,
currency: data.amount.currency,
returnUrl: data.successUrl,
});
// Record an initial Transaction on the commercetools Payment
await this.addPaymentTransaction(payment, {
transactionId: result.transactionId,
state: "Pending",
transactionStatus: result.status,
transactionCode: result.statusCode,
});
return {
status: "OK",
payload: { redirectURL: result.redirectUrl },
};
};
public readonly getPaymentMethods: PaymentAdapter["getPaymentMethods"] =
async (source, device) => {
const methods = await this.acmeApi.listMethods();
return methods.map((method) => ({
id: method.id,
name: { "en-GB": method.label },
}));
};
}
```
`addPaymentTransaction` records the transaction, sets the status interface code/text, and transitions the Payment state — you don’t write raw update actions.
### Wrap the adapter in a module
[Section titled “Wrap the adapter in a module”](#wrap-the-adapter-in-a-module)
The module extends `AbstractModule` from `@evolve-framework/core`. The PSP client is built in `init()` (constructors must stay cheap and side-effect free), and `getHttpConfig()` registers the routes:
src/module.ts
```typescript
import type { ClientFactory } from "@evolve-framework/commercetools";
import { paymentServiceHandlers } from "@evolve-framework/commercetools/payments";
import { AbstractModule, type HttpConfig } from "@evolve-framework/core";
import { AcmePaymentAdapter } from "#src/adapter.ts";
import { createAcmeClient, type AcmeClient, type AcmeConfig } from "#src/lib/client.ts";
import { createHealthcheckHandler } from "#src/routes/healthcheck.ts";
import { createPushHandler } from "#src/routes/push.ts";
import { createRedirectHandler } from "#src/routes/redirect.ts";
export type AcmePaymentModuleOptions = {
config: AcmeConfig;
clientFactory: ClientFactory;
internalRoutes?: boolean;
externalRoutes?: boolean;
};
export class AcmePaymentModule extends AbstractModule {
private readonly moduleOptions: AcmePaymentModuleOptions;
private acmeClient!: AcmeClient;
constructor(options: AcmePaymentModuleOptions) {
super();
this.moduleOptions = options;
}
override init(): void {
this.acmeClient = createAcmeClient(this.moduleOptions.config);
}
override getHttpConfig(): HttpConfig {
const { config, clientFactory } = this.moduleOptions;
const adapter = new AcmePaymentAdapter(
clientFactory.getSystemRequestBuilder(),
this.acmeClient,
);
return {
routes: (app): void => {
app.get("/healthcheck", createHealthcheckHandler());
if (this.moduleOptions.internalRoutes ?? true) {
app.register(paymentServiceHandlers, { paymentAdapter: adapter });
}
if (this.moduleOptions.externalRoutes ?? true) {
app.post("/push", createPushHandler({ adapter, webhookSecret: config.webhookSecret }));
app.get("/redirect", createRedirectHandler(adapter));
}
},
};
}
}
```
The routes split into two groups:
| Route | Group | Purpose |
| ----------------------- | -------- | ---------------------------------------------------- |
| `POST /payment-methods` | internal | Return available methods for the given cart or order |
| `POST /create` | internal | Start a transaction and return a redirect URL |
| `POST /push` | external | Receive webhook callbacks from the PSP |
| `GET /redirect` | external | Handle customer redirect back from the PSP |
The internal routes are the checkout-facing contract and come for free from `paymentServiceHandlers` — they validate requests against the shared TypeBox schemas and delegate to your adapter. Only the external, PSP-facing routes are handwritten. The `/push` handler must verify webhook authenticity; if your PSP requires raw body verification (e.g., Stripe), register `fastify-raw-body` inside `getHttpConfig()` — see `StripePaymentModule` in `packages/commercetools-stripe/src/module.ts` for the pattern.
Publish the package to your own registry, or keep it as a workspace package inside your project’s monorepo — either way, pin it in the `catalog:` section of `pnpm-workspace.yaml` like the other framework packages. If the provider is broadly useful, consider submitting it upstream as an official `@evolve-framework/commercetools-` package.
## 2. Create the thin platform service
[Section titled “2. Create the thin platform service”](#2-create-the-thin-platform-service)
The platform side is a small mount at `backend/services/payment-commercetools-/` with just four source files plus `run.ts` and `terraform/`. The config extends `CommercetoolsConfig`, and the server wires the module into a `DomainService`:
src/server.ts
```typescript
import { AcmePaymentModule } from "@evolve-framework/commercetools-acme";
import { createDomainService } from "@evolve-framework/core";
import { clientFactory, initClientFactory } from "#src/commercetools.ts";
import { config, loadConfig } from "#src/config.ts";
export const startServer = async (): Promise => {
await loadConfig();
await initClientFactory();
const service = await createDomainService({
name: "payment-commercetools-acme",
module: new AcmePaymentModule({
config: {
apiKey: config.ACME_API_KEY,
webhookSecret: config.ACME_WEBHOOK_SECRET,
serverUrl: config.SERVER_URL,
},
clientFactory,
internalRoutes: true,
externalRoutes: true,
}),
http: {
address: {
host: config.HTTP_HOST,
port: config.HTTP_PORT,
},
},
});
await service.start();
};
```
The `ClientFactory` comes from `@evolve-framework/commercetools`:
src/commercetools.ts
```typescript
import {
ClientFactory,
getClientOptions,
} from "@evolve-framework/commercetools";
import { config } from "#src/config.ts";
export let clientFactory!: ClientFactory;
export const initClientFactory = async (): Promise => {
const options = await getClientOptions(config);
clientFactory = ClientFactory.create(options);
};
```
Add the framework package to the service’s `package.json` with `catalog:` as the version and add the pinned version to the `catalog:` section of `pnpm-workspace.yaml`. Compare with `backend/services/payment-commercetools-stripe/src/` for the complete set of files.
The `internalRoutes`/`externalRoutes` flags exist so you can split the deployment: one instance exposed only to checkout, another exposed publicly for webhooks and redirects. Locally, run both in one process.
## 3. Register with the checkout service
[Section titled “3. Register with the checkout service”](#3-register-with-the-checkout-service)
Add your provider’s endpoint to the checkout service configuration so it knows where to route payment requests:
```bash
CHECKOUT_PAYMENT_SERVICE_ENDPOINTS='{"acme": "http://payment-acme:3000"}'
```
The checkout service builds a `PaymentProvider` for each entry in this map. `availablePaymentMethods` fans out `POST /payment-methods` across all providers and prefixes each method id with the provider identifier (`acme-ideal`); `createPayment` and `checkoutComplete` route `POST /create` to the provider matching the selected method.
## 4. Handle payment state transitions
[Section titled “4. Handle payment state transitions”](#4-handle-payment-state-transitions)
Update the commercetools Payment state to one of the standard states:
| State | When |
| ------------------ | ---------------------------------------------------- |
| `PaymentInitial` | Payment created (set automatically by API extension) |
| `PaymentPending` | Transaction started, awaiting PSP result |
| `PaymentSuccess` | PSP confirms payment succeeded |
| `PaymentCancelled` | Customer cancelled at PSP |
| `PaymentFailure` | PSP reports a failure |
When the state changes, commercetools emits a `PaymentStatusStateTransition` event. The checkout service subscribes to this event and updates the order accordingly (e.g., marking it as paid when all payments succeed).
## 5. Add Terraform
[Section titled “5. Add Terraform”](#5-add-terraform)
Define the service infrastructure and commercetools payment states in Terraform. Follow the patterns in existing payment services such as `backend/services/payment-commercetools-stripe/terraform/`.
## Further reading
[Section titled “Further reading”](#further-reading)
* [Payment architecture](/architecture/payment/) for the full payment flow and state machine
* [REST endpoints and webhooks](/architecture/infrastructure/rest-webhooks/) for webhook verification patterns per provider
* [Messaging and events](/architecture/infrastructure/messaging/) for how payment state transitions trigger downstream processing
# Adding a search engine
> How to add support for a different search engine by creating an implementation package.
Evolve’s three-layer architecture separates schema definitions from implementation. The catalog module defines search interfaces (product search, facets, categories) in `@evolve-framework/schemas`, while the actual search logic lives in an implementation package. This guide walks through creating a new implementation for a search engine such as Typesense, Meilisearch, or Elasticsearch.
1. **Create the implementation package**
Create a new package in your project repository that depends on the core and schemas layers:
* packages/search-typesense/
* src/
* index.ts
* module.ts
* search-provider.ts
* mappers/
* product.ts
* client.ts
* package.json
```json
{
"name": "@evolve-packages/search-typesense",
"dependencies": {
"@evolve-framework/core": "catalog:",
"@evolve-framework/schemas": "catalog:",
"@evolve-framework/commercetools": "catalog:",
"typesense": "^1.8.0"
}
}
```
The `@evolve-framework/*` packages are published to the private registry and pinned centrally in `pnpm-workspace.yaml` under `catalog:`, so the package uses the `catalog:` protocol instead of version numbers.
2. **Implement a SearchProvider**
The catalog module delegates search to a `SearchProvider` abstraction, defined in `@evolve-framework/commercetools` (`src/lib/search/base.ts`), with the built-in Algolia and commercetools providers as reference implementations. Create a provider with the same shape that implements search against your engine:
src/search-provider.ts
```typescript
export class TypesenseSearchProvider {
async searchProducts(context, args) {
const client = this.createClient(context.config);
const results = await client.collections("products").documents().search({
q: args.query,
filter_by: buildFilters(args.filters, context.storeContext),
sort_by: mapSortOrder(args.sort),
page: args.page,
per_page: args.pageSize,
facet_by: args.facets?.join(","),
});
return {
results: results.hits.map(mapSearchHitToProduct),
total: results.found,
facets: mapFacets(results.facet_counts),
};
}
async searchCategories(context, args) {
// Implement category search
}
private createClient(config: Record) {
return new Typesense.Client({
nodes: [{ host: config.typesenseHost, port: 443, protocol: "https" }],
apiKey: config.typesenseApiKey,
});
}
}
```
Note that the `results` field (not `items`) and `facets` field match the `SearchResult` type defined alongside the `SearchProvider` abstraction.
3. **Create the GraphQL module**
Subclass `CatalogModule` and override the search-related resolvers. `CatalogModule` builds its resolver map in its constructor, so replace the `productSearch` resolver after calling `super`:
src/module.ts
```typescript
import {
CatalogModule,
type ClientFactory,
} from "@evolve-framework/commercetools";
import { TypesenseSearchProvider } from "./search-provider.ts";
export class TypesenseCatalogModule extends CatalogModule {
constructor(options: { clientFactory: ClientFactory }) {
super(options);
const provider = new TypesenseSearchProvider();
this.resolvers.Query.productSearch = async (_parent, args, context) => {
return provider.searchProducts(context, args);
};
}
}
```
This replaces only the search resolvers. Product detail pages, categories, and other catalog queries continue to use the default commercetools resolvers.
4. **Wire into the catalog service**
In the catalog service’s `module.ts`, swap the default module for your new one:
backend/services/catalog-commercetools/src/module.ts
```typescript
import { CompositeModule } from "@evolve-framework/core";
import { TypesenseCatalogModule } from "@evolve-packages/search-typesense";
import { clientFactory } from "#src/commercetools.ts";
export const createModule = (): CompositeModule => {
return new CompositeModule([new TypesenseCatalogModule({ clientFactory })]);
};
```
The search engine configuration goes through the per-request `config` object built in the service’s `src/context.ts`:
backend/services/catalog-commercetools/src/context.ts
```typescript
config: {
searchEngine: "typesense",
typesenseHost: config.TYPESENSE_HOST,
typesenseApiKey: config.TYPESENSE_API_KEY,
},
```
This is exposed as `context.config` so your search provider can access the credentials at request time.
5. **Sync product data**
Search engines need a product feed. Evolve publishes `product-published` events when products change. Create an event handler that indexes products into your search engine. See [custom events](/how-to/custom-events/) for how to wire a handler.
6. **Add credentials to Terraform**
Store the search engine API key and host as secrets and inject them as environment variables. Follow the patterns in the existing Algolia configuration in `terraform/`.
## Further reading
[Section titled “Further reading”](#further-reading)
* [Framework architecture](/architecture/core/framework/architecture/) for the three-layer design and module system
* [Customization](/architecture/core/framework/customization/) for overriding resolvers and subclassing modules
* [Messaging and events](/architecture/infrastructure/messaging/) for product sync through the event bus
# Adding a field to the checkout
> How to add a custom field (such as a purchase order number) to the checkout flow.
Projects commonly need extra fields in the checkout, such as a purchase order number, delivery instructions, or a VAT ID. This guide walks through adding a custom field end to end: schema, resolver, form component, validation, and persistence.
1. **Define the custom type in Terraform**
Register the custom field on the order resource in commercetools through Terraform:
```hcl
resource "commercetools_type" "order_custom_fields" {
key = "order-custom-fields"
name = { en = "Order custom fields" }
resource_type_ids = ["order"]
field {
name = "purchaseOrderNumber"
label = { en = "Purchase order number" }
type { name = "String" }
}
}
```
2. **Extend the GraphQL schema**
Create a project-specific module that adds the field to the schema. Extend `AbstractModule` and compose it alongside the framework modules:
```typescript
import { AbstractModule } from "@evolve-framework/core";
import { gql } from "graphql-tag";
export class OrderCustomFieldsModule extends AbstractModule {
typedefs = gql`
extend type Order {
purchaseOrderNumber: String
}
`;
resolvers = {
Order: {
purchaseOrderNumber: (order) =>
order.custom?.fields?.purchaseOrderNumber ?? null,
},
};
}
```
Add it to the checkout service’s module composition:
backend/services/checkout-commercetools/src/module.ts
```typescript
export const createModule = (): CompositeModule => {
return new CompositeModule([
new CheckoutModule({ clientFactory }),
new PaymentModule({ clientFactory }),
new CartModule({ clientFactory }),
new OrderCustomFieldsModule(),
]);
};
```
3. **Run codegen**
After updating the schema, run GraphQL Code Generator to produce updated TypeScript types for the frontend:
```bash
pnpm codegen
```
4. **Add the form component**
The checkout is structured as separate step components, each with their own form and Zod validation. Create a new field component using React Hook Form:
frontend/site/src/components/checkout/purchase-order-field.tsx
```typescript
import { useFormContext } from "react-hook-form";
export const PurchaseOrderField = () => {
const { register, formState: { errors } } = useFormContext();
return (
{errors.purchaseOrderNumber && (
{errors.purchaseOrderNumber.message}
)}
);
};
```
5. **Add Zod validation**
Add the field to the relevant checkout step’s validation schema:
```typescript
import { z } from "zod";
export const orderDetailsSchema = z.object({
// ... existing fields for this step
purchaseOrderNumber: z
.string()
.max(50, "PO number must be 50 characters or fewer")
.optional(),
});
```
React Hook Form uses this schema via `zodResolver` to validate the field before submission.
6. **Persist the value**
Save the field through a commercetools `setCustomField` action. In the checkout service, add a resolver or API extension handler that writes the value when the order is created:
```typescript
const updateAction = {
action: "setCustomField",
name: "purchaseOrderNumber",
value: input.purchaseOrderNumber,
};
```
## Further reading
[Section titled “Further reading”](#further-reading)
* [Data model](/architecture/core/datamodel/) for how Evolve’s vendor-independent schema supports extending types
* [Customization](/architecture/core/framework/customization/) for adding project-specific modules
* [Frontend development](/evolve/frontend/) for component patterns, form handling, and data fetching
# Contributing
Once your team forks Evolve as the starting point for a project, the repository comes with a contribution workflow you can adopt as-is or adapt to your own conventions.
## Development workflow
[Section titled “Development workflow”](#development-workflow)
* Branch from `main` using the `feat/`, `fix/`, or `chore/` prefix conventions, and use conventional-commit messages.
* Keep changes reviewable: one concern per pull request.
## Before you submit
[Section titled “Before you submit”](#before-you-submit)
1. Make sure your environment is set up (see [getting started](/evolve/getting-started/)).
2. Run `pnpm check` to verify formatting, types, and linting pass.
3. Add a changelog entry if your change is user-facing (see [changelog](/evolve/changelog/)).
# Creating a new Evolve project
> How to create a new project based on Evolve.
When creating a new project based on Evolve, it is important to take care of a few things. This chapter will explain how to do this.
## Fork, no history
[Section titled “Fork, no history”](#fork-no-history)
We don’t want to have the history of Evolve in the project since that can lead to situations where unwanted information is leaked in the commit history.
## Step-by-step
[Section titled “Step-by-step”](#step-by-step)
1. Create a clean clone of Evolve:
```bash
git clone git@github.com:labdigital-evolve/evolve-platform.git
cd evolve-platform
rm -rf .git
```
2. Remove files not relevant for the project (for example):
```bash
rm -rf docs
```
3. Filter out backend services not relevant for the project. For example, if using Contentful instead of Storyblok:
```bash
rm -rf backend/services/cms-storyblok
```
4. Or remove payment providers you don’t need:
```bash
rm -rf backend/services/payment-commercetools-buckaroo
rm -rf backend/services/payment-commercetools-paynl
```
5. Now init the repository:
```bash
git init && git commit -m "Initial commit"
```
6. As a final step, create a tag in Evolve so that you can see what version of Evolve was used to create the project.
# Creating a new domain service
> How to add a new backend domain service to the Evolve platform.
Every backend service in Evolve follows the same structure: `config.ts` declares typed environment configuration, `module.ts` composes the framework modules the service exposes, `server.ts` boots them as a `DomainService`, and `index.ts` is a thin entry point. All heavy lifting — the GraphQL server, HTTP routes, process lifecycle — comes from the published `@evolve-framework/*` packages. This guide walks through creating a service from scratch, using the real services (`quotes-commercetools` is the smallest complete example) as reference.
## 1. Scaffold the service
[Section titled “1. Scaffold the service”](#1-scaffold-the-service)
Create a new directory under `backend/services/`:
* backend/services/loyalty/
* src/
* index.ts
* config.ts
* module.ts
* server.ts
* context.ts
* modules/
* loyalty.ts
* run.ts
* terraform/
* …
* package.json
Framework packages are published to the private registry and pinned centrally in `pnpm-workspace.yaml` under `catalog:`, so dependencies use the `catalog:` protocol:
```json
{
"dependencies": {
"@evolve-framework/core": "catalog:",
"@evolve-framework/commercetools": "catalog:",
"@labdigital/enviconf": "catalog:",
"graphql": "catalog:",
"graphql-tag": "catalog:",
"graphql-yoga": "catalog:"
},
"scripts": {
"dev": "node --env-file=../../../.env --watch ./run.ts",
"codegen": "graphql-codegen",
"build": "node ./build.ts",
"start": "node ./dist/server/index.mjs"
}
}
```
## 2. Declare the configuration
[Section titled “2. Declare the configuration”](#2-declare-the-configuration)
Configuration is a typed [enviconf](https://github.com/labdigital/enviconf) class. Extend `CommercetoolsConfig` when the service talks to commercetools (it brings the `CTP_*` fields), or `BaseConfig` otherwise:
src/config.ts
```typescript
import { CommercetoolsConfig } from "@evolve-framework/commercetools";
import { type EnviConfig, envfield } from "@labdigital/enviconf";
class Config extends CommercetoolsConfig {
readonly COMPONENT_NAME = "loyalty";
readonly HTTP_HOST = "localhost";
readonly HTTP_PORT = 4008;
readonly REDIS_URL: string | undefined;
config(): EnviConfig {
return {
...super.config(),
COMPONENT_NAME: envfield.string(),
HTTP_HOST: envfield.string(),
HTTP_PORT: envfield.number(),
REDIS_URL: envfield.string({ optional: true }),
};
}
}
export const config = new Config();
export const loadConfig = async (): Promise => {
config.load({ prefix: "LOYALTY_" });
};
```
## 3. Define and compose modules
[Section titled “3. Define and compose modules”](#3-define-and-compose-modules)
A module bundles a GraphQL schema, its resolvers, and optional HTTP routes. Most services only *instantiate* modules that ship with the framework (`QuoteModule`, `CatalogModule`, `ContentfulModule`, …), but you can write your own by extending `AbstractModule`:
src/modules/loyalty.ts
```typescript
import { AbstractModule } from "@evolve-framework/core";
import { gql } from "graphql-tag";
export class LoyaltyModule extends AbstractModule {
typedefs = gql`
type LoyaltyAccount {
points: Int!
tier: String!
}
extend type Customer {
loyalty: LoyaltyAccount
}
`;
resolvers = {
Customer: {
loyalty: loyaltyResolver,
},
};
}
```
`module.ts` composes everything the service exposes into a single subgraph via `CompositeModule`. This is also the file where deploy-time choices live — adding or removing a module here changes what the service serves:
src/module.ts
```typescript
import { CompositeModule } from "@evolve-framework/core";
import { LoyaltyModule } from "./modules/loyalty.ts";
export const createModule = (): CompositeModule => {
return new CompositeModule([new LoyaltyModule()]);
};
```
Modules can also expose REST routes and Fastify plugins through `getHttpConfig()` — see the account service’s auth endpoints or the payment provider modules for examples. Everything a module returns from `getGraphQLConfig()` and `getHttpConfig()` is mounted automatically by the `DomainService`.
## 4. Wire the service files
[Section titled “4. Wire the service files”](#4-wire-the-service-files)
**Server** — `createDomainService` takes the module and boots the GraphQL (Yoga) and HTTP (Fastify) servers around it. Process lifecycle — signal handling and graceful shutdown — is managed internally, and module `init()`/ `close()` hooks run at the right moments:
src/server.ts
```typescript
import {
createDomainService,
type DomainService,
} from "@evolve-framework/core";
import { useClientContext } from "@evolve-framework/core/service/graphql";
import { config, loadConfig } from "./config.ts";
import { createContext } from "./context.ts";
import { createModule } from "./module.ts";
export const createApp = async (): Promise => {
await loadConfig();
return createDomainService({
name: config.COMPONENT_NAME,
module: createModule(),
graphql: {
context: createContext,
plugins: [useClientContext()],
},
http: {
address: {
host: config.HTTP_HOST,
port: config.HTTP_PORT,
},
},
});
};
export const startServer = async (): Promise => {
const service = await createApp();
await service.start();
};
```
Keeping `createApp` separate from `startServer` lets tests boot the service without binding a port and use `service.fastify.inject(...)`.
**Entry point** is a thin wrapper that both exports and calls `startServer` (the export is what the monolith’s worker threads invoke):
src/index.ts
```typescript
import { startServer } from "./server.ts";
export { startServer } from "./server.ts";
await startServer();
```
**Local development** entry initializes observability first:
run.ts
```typescript
import { initObservability } from "@evolve-framework/core/observability";
initObservability();
await import("./src/index.ts");
```
**Request context** builds the per-request GraphQL context. For commercetools-backed services the standard shape wires the store context and federated client context:
src/context.ts
```typescript
import {
ClientContext,
RemoteClientContextLoader,
StoreContext,
} from "@evolve-framework/commercetools";
import { logger } from "@evolve-framework/core/observability/logging";
import {
readStoreContextFromRequest,
type ServerContext,
} from "@evolve-framework/core/service/graphql";
import type { YogaInitialContext } from "graphql-yoga";
import { clientFactory } from "./commercetools.ts";
import { config } from "./config.ts";
export const createContext = async (
serverContext: ServerContext & YogaInitialContext,
) => {
const { request } = serverContext;
const commercetoolsClient = clientFactory.getSystemRequestBuilder();
const storeContext = new StoreContext(
readStoreContextFromRequest(request),
commercetoolsClient,
);
const loader = new RemoteClientContextLoader(
config.ACCOUNT_SERVICE_ENDPOINT,
clientFactory,
);
const clientContext = new ClientContext(storeContext, loader);
return {
...serverContext,
log: logger,
storeContext,
clientContext,
globalScopedClient: () => commercetoolsClient,
};
};
```
See `backend/services/quotes-commercetools/src/context.ts` for the complete version including data loaders and the federated token.
## 5. Register for local development
[Section titled “5. Register for local development”](#5-register-for-local-development)
The monolith boots every service module in its own worker thread during `pnpm dev`. Add the service in `backend/services/monolith/src/index.ts` — both the side-effect import at the top *and* the `moduleNames` array — plus a `workspace:*` dependency in the monolith’s `package.json`:
```typescript
import "@evolve-platform/loyalty";
// ...
const moduleNames = [
// ...
"@evolve-platform/loyalty",
];
```
Then recompose the local supergraph so the Hive Router picks up the new subgraph. Add the service to the composition task in `backend/services/graphql-gateway/Taskfile.yaml` and run:
```bash
task -d backend/services/graphql-gateway supergraph
```
In production the router polls the supergraph from the Hive CDN; the new subgraph joins it through the `hive_schema_publish` Terraform resource below.
## 6. Add Terraform
[Section titled “6. Add Terraform”](#6-add-terraform)
Each service has a `terraform/` directory with per-cloud subdirectories. All three clouds follow the same lifecycle (schema check, deploy, schema publish) but use different compute primitives.
### Directory structure
[Section titled “Directory structure”](#directory-structure)
* terraform/
* aws/
* main.tf ECS service + Hive schema check/publish
* locals.tf Service name, image, env\_vars
* variables.tf Inputs from Mach Composer
* data.tf SSM parameter lookups
* commercetools.tf CT client credentials (Secrets Manager)
* schema.generated.graphql Generated schema (used by Hive)
* outputs.tf
* versions.tf
* azure/
* main.tf Container Apps module + Hive lifecycle
* locals.tf Service name, image, env\_vars
* variables.tf Inputs from Mach Composer
* data.tf Resource group, ACR, Redis lookups
* secrets.tf Key Vault + role assignments
* roles.tf User Assigned Identity + RBAC
* commercetools.tf CT client credentials (Key Vault)
* schema.generated.graphql Generated schema (used by Hive)
* outputs.tf
* versions.tf
* gcp/
* main.tf Cloud Run module + Hive lifecycle
* locals.tf Service name, image, env\_vars
* variables.tf Inputs from Mach Composer
* data.tf google\_client\_config lookup
* commercetools.tf CT client credentials (Secret Manager)
* schema.generated.graphql Generated schema (used by Hive)
* outputs.tf
* versions.tf
### The Azure pattern (primary example)
[Section titled “The Azure pattern (primary example)”](#the-azure-pattern-primary-example)
**`main.tf`**: every service follows a three-phase lifecycle:
1. **Schema check**: `hive_schema_check` validates the GraphQL schema against the registry before deployment
2. **Deploy**: the compute module (Container Apps on Azure) deploys the container image
3. **Schema publish**: `hive_schema_publish` registers the live endpoint after the service is up
```hcl
resource "hive_schema_check" "graphql_schema_check" {
service = local.service_name
commit = var.component_version
schema = file("${path.module}/schema.generated.graphql")
context_id = "${local.service_name}/${var.component_version}"
}
module "service" {
source = "evolve-platform/app-container/azurerm"
version = "0.2.2"
tags = local.tags
name = "${var.azure.resource_prefix}-${local.service_name}"
cpu = var.variables.cpu
memory = var.variables.memory
min_replicas = var.variables.min_replicas
max_replicas = var.variables.max_replicas
container_app_environment_id = data.azurerm_container_app_environment.primary.id
resource_group_name = data.azurerm_resource_group.primary.name
image = local.image
identity_id = azurerm_user_assigned_identity.app.id
env_vars = local.env_vars
secrets = [
{
secret_id = module.commercetools_server_token.secret_id
secret_name = module.commercetools_server_token.secret_name
env_name = "CTP_CLIENT_SECRET"
}
]
healthcheck = {
path = "/healthcheck"
}
depends_on = [
azurerm_role_assignment.app_acrpull,
hive_schema_check.graphql_schema_check,
]
}
resource "hive_schema_publish" "graphql_schema_publish" {
service = local.service_name
commit = var.component_version
url = local.service_graphql_endpoint
schema = file("${path.module}/schema.generated.graphql")
depends_on = [module.service]
}
```
**`locals.tf`**: defines the service name, container image, and all environment variables. Secrets are never placed in `env_vars`; they go through Key Vault references in the `secrets` block instead:
```hcl
locals {
service_name = "loyalty"
image = "${local.container_registry_name}.azurecr.io/${local.service_name}:${var.component_version}"
env_vars = {
NODE_ENV = "production"
SERVICE_NAME = local.service_name
SITE = var.site
# ... service-specific env vars
}
}
```
**`secrets.tf`**: creates a per-service Key Vault and assigns access roles for the deploy identity and the app identity:
```hcl
module "keyvault" {
source = "evolve-platform/key-vault/azurerm"
version = "0.1.1"
tags = local.tags
name = "${var.azure.resource_prefix}-${local.service_name_short}"
tenant_id = data.azurerm_client_config.current.tenant_id
resource_group_name = data.azurerm_resource_group.primary.name
location = data.azurerm_resource_group.primary.location
}
# The deploy identity gets Key Vault Administrator to manage secrets during CI.
# The app identity gets Key Vault Secrets User for read-only runtime access.
resource "azurerm_role_assignment" "keyvault_app" {
scope = module.keyvault.key_vault_id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_user_assigned_identity.app.principal_id
}
```
**`roles.tf`**: creates a User Assigned Identity and grants it `acrpull` on the container registry plus any data-plane access (e.g. Redis `Data Owner`):
```hcl
resource "azurerm_user_assigned_identity" "app" {
name = "${var.azure.resource_prefix}-${local.service_name}-uai"
resource_group_name = data.azurerm_resource_group.primary.name
location = data.azurerm_resource_group.primary.location
tags = local.tags
}
resource "azurerm_role_assignment" "app_acrpull" {
scope = data.azurerm_container_registry.acr.id
role_definition_name = "acrpull"
principal_id = azurerm_user_assigned_identity.app.principal_id
}
```
### AWS and GCP alternatives
[Section titled “AWS and GCP alternatives”](#aws-and-gcp-alternatives)
The AWS and GCP directories follow the same structure but use different compute modules:
| Cloud | Module | Compute |
| ----- | ------------------------------------------ | -------------- |
| Azure | `evolve-platform/app-container/azurerm` | Container Apps |
| AWS | `evolve-platform/ecs-service/aws` | ECS Fargate |
| GCP | `evolve-platform/cloud-run-service/google` | Cloud Run |
Secret management also differs per cloud: Azure uses Key Vault, AWS uses Secrets Manager (referenced via `CTP_CREDENTIALS_SECRET_NAME`), and GCP uses Secret Manager with Cloud Run secret mounts.
Reference an existing service’s terraform directories for the full per-cloud boilerplate: `backend/services/order-commercetools/terraform/`.
### Register in Mach Composer
[Section titled “Register in Mach Composer”](#register-in-mach-composer)
Add the component to the site configuration YAML. This registers the Terraform module with Mach Composer and wires up variables and secrets:
First, register the component. Azure configs use a shared `_components.yaml` file (referenced via `$ref`), while AWS and GCP define components inline:
```yaml
# config/azure/_components.yaml (or inline in aws/demo.yaml)
- name: loyalty
source: ../../backend/services/loyalty/terraform/azure/
version: "$LATEST"
branch: "main"
integrations:
- azure
- commercetools
- sentry
- hive
```
Then add the site-level config that passes variables and secrets:
```yaml
# In the site's components list
- name: loyalty
variables:
account_service_endpoint: ${component.account-commercetools.endpoint}
secrets:
hive_token: ${var.secrets.hive.api_token}
```
The `integrations` list tells Mach Composer to inject the relevant provider configuration (e.g. `commercetools` injects `ct_project_key` and related variables). The `${component.*}` syntax references outputs from other components. Note that output names differ per cloud: Azure uses `.endpoint`, AWS uses `.service_endpoint`, and GCP uses `.url`.
## Further reading
[Section titled “Further reading”](#further-reading)
* [Framework architecture](/architecture/core/framework/architecture/) for the module system and `DomainService` design
* [Customization](/architecture/core/framework/customization/) for overriding resolvers and extending schemas
# Adding a custom event and handler
> How to define a custom event, publish it from a service, and handle it in a consumer.
Evolve uses event-driven messaging for asynchronous communication between services. This guide walks through adding a new internal event type, publishing it from a service, and consuming it in a handler.
1. **Define the event schema**
Events are defined as JSON Schema files in the shared `schemas/` directory at the repository root. Zod validators are generated from them per service:
schemas/src/events/inventory-low\.schema.json
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://schemas.evolve.labdigital.nl/events/inventory-low",
"title": "InventoryLowEvent",
"type": "object",
"allOf": [
{ "$ref": "https://schemas.evolve.labdigital.nl/base.schema.json" }
],
"properties": {
"type": { "type": "string", "const": "inventory-low" },
"sku": { "type": "string" },
"availableQuantity": { "type": "number" },
"threshold": { "type": "number" },
"storeKey": { "type": "string" }
},
"required": ["type", "sku", "availableQuantity", "threshold", "storeKey"]
}
```
The `base.schema.json` reference adds the shared envelope fields (`type`, `origin`, `timestamp`). Register the schema URI in the publishing service’s `codegen.types.ts` and run `pnpm codegen` — this generates the `inventoryLowEventSchema` Zod validator and `InventoryLowEvent` type into the service’s `src/generated/events.ts` via `@evolve-framework/json-schema-to-zod`.
2. **Publish the event**
Use `resolvePublisher` from `@evolve-packages/cloud-adapters` to obtain a `MessagePublisher` for the configured target. It inspects the target URL to select the correct cloud provider (EventBridge/SQS, Event Grid/Service Bus, Pub/Sub, or Redis for local development):
```typescript
import { resolvePublisher } from "@evolve-packages/cloud-adapters";
import { inventoryLowEventSchema } from "#src/generated/events.ts";
const publisher = resolvePublisher(config.INTERNAL_EVENTS_TARGET);
const event = inventoryLowEventSchema.parse({
type: "inventory-low",
sku: variant.sku,
availableQuantity: variant.quantity,
threshold: 10,
storeKey: storeContext.storeKey,
origin: "catalog-commercetools",
timestamp: new Date().toISOString(),
});
await publisher.publish(event, {
type: event.type,
origin: event.origin,
messageGroupId: variant.sku,
});
```
The `type` and `origin` are passed as message attributes for routing. The `messageGroupId` ensures ordering per SKU on transports that support it.
3. **Create the event handler**
Add a handler function that processes the event. The handler receives the event payload directly (not wrapped in a `.data` property). Use a switch on `event.type` to dispatch:
```typescript
import type { CloudEventsPayload } from "@commercetools/platform-sdk";
const handleInternalEvent = async (event: CloudEventsPayload) => {
switch (event.type) {
case "inventory-low":
return handleInventoryLow(event);
// ... other event types
}
};
const handleInventoryLow = async (event) => {
await notifyOpsTeam(event.sku, event.availableQuantity);
};
```
4. **Wire the cloud trigger**
Each cloud adapter package has a handler factory that wraps the trigger into the common callback signature:
**AWS (SQS + Lambda)**:
```typescript
import { createSQSHandler } from "@evolve-framework/cloud-adapter-aws";
import { lambdaHandlerFactory } from "@evolve-framework/core/observability/lambda";
const sqsHandler = createSQSHandler(handleInternalEvent);
export const handler = lambdaHandlerFactory(
"Internal event handler",
() => sqsHandler,
);
```
**Azure (Service Bus)**:
```typescript
import { createServiceBusHandler } from "@evolve-framework/cloud-adapter-azure";
app.serviceBusQueue(`${config.COMPONENT_NAME}-events`, {
handler: createServiceBusHandler(handleInternalEvent),
connection: "SERVICE_BUS_CONNECTION_STRING",
queueName: config.QUEUE_NAME,
cardinality: "one",
});
```
**GCP (Pub/Sub)**:
```typescript
import { createPubSubHandler } from "@evolve-framework/cloud-adapter-gcp";
const pubsubHandler = createPubSubHandler((event) => {
return handleInternalEvent(event as CloudEventsPayload);
});
await pubsubHandler.start({
path: "/internal-events",
httpHost: config.HTTP_HOST,
httpPort: config.HTTP_PORT,
});
```
5. **Configure the queue in Terraform**
Create the cloud queue and subscribe it to the internal event bus. For AWS:
```hcl
resource "aws_sqs_queue" "inventory_events" {
name = "${var.component_name}-inventory-events"
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.inventory_events_dlq.arn
maxReceiveCount = 4
})
}
```
Always include a dead-letter queue for messages that fail after the maximum number of delivery attempts.
6. **Test locally**
For local development, the framework uses Redis as its transport. The `startDevelopmentListener` polls the queue with `brPop` so you can test the full publish/consume cycle without cloud infrastructure:
```typescript
import { startDevelopmentListener } from "@evolve-framework/core/messaging";
startDevelopmentListener(
config.REDIS_URL,
async (event) => {
await handleInternalEvent(event as CloudEventsPayload);
},
{ continueOnError: true, dlq: `${config.COMPONENT_NAME}-dlq` },
);
```
## Further reading
[Section titled “Further reading”](#further-reading)
* [Messaging and events](/architecture/infrastructure/messaging/) for the full event architecture, provider details, and built-in event types
# Customizing email templates
> Add a new transactional email notification end to end, from JSON Schema to React Email template.
This guide walks through adding a new transactional email notification to Evolve. You will define the data schema, generate types, wire the notification into a domain service, create the email template, and test locally.
See [Email & notifications](/architecture/infrastructure/email/) for the architecture overview.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* A running dev environment (`pnpm dev`)
* Docker services started (`task docker:services`). Mailpit runs on `localhost:8025`
## 1. Define the JSON Schema
[Section titled “1. Define the JSON Schema”](#1-define-the-json-schema)
Create a new schema file in `schemas/src/notifications/`. This defines the notification type and the variables available to the email template.
For example, a shipment-delayed notification:
schemas/src/notifications/user-shipment-delayed.schema.json
```json
{
"$id": "user-shipment-delayed",
"type": "object",
"allOf": [{ "$ref": "https://schemas.evolve.labdigital.nl/notifications/base.schema.json" }],
"properties": {
"type": { "type": "string", "const": "shipment-delayed" },
"variables": {
"type": "object",
"properties": {
"orderNumber": { "type": "string" },
"expectedDate": { "type": "string" },
"reason": { "type": "string" }
},
"required": ["orderNumber", "expectedDate"]
}
},
"required": ["type", "variables"]
}
```
The `base.schema.json` provides the shared envelope fields: `recipients` and `storeContext`.
## 2. Generate types
[Section titled “2. Generate types”](#2-generate-types)
Register the schema URI in `backend/services/email-smtp/codegen.types.ts`, then run codegen to produce the Zod schema and TypeScript types from your new JSON Schema:
```bash
pnpm codegen
```
This generates a Zod validator and a TypeScript type (e.g. `UserShipmentDelayedNotification`) into `email-smtp/src/generated/notifications.ts` that are used for compile-time and runtime validation.
## 3. Add a typed method to `QueuedUserNotificationClient`
[Section titled “3. Add a typed method to QueuedUserNotificationClient”](#3-add-a-typed-method-to-queuedusernotificationclient)
Open `queued-user-notification-client.ts` in the `@evolve-framework/user-notifications` package (in the framework repository, `packages/user-notifications/src/`) and add a convenience method:
```typescript
async shipmentDelayed(
params: UserShipmentDelayedNotification
): Promise {
await this.queueMessage(
"shipment-delayed",
params.recipients,
params.storeContext,
params.variables,
);
}
```
This ensures callers get full type checking for the notification payload.
## 4. Publish from the domain service
[Section titled “4. Publish from the domain service”](#4-publish-from-the-domain-service)
In the service where the business event happens, call the new method:
```typescript
await notificationClient.shipmentDelayed({
recipients: [{ email: customer.email, name: customer.name }],
storeContext: { storeKey, locale },
variables: {
orderNumber: order.orderNumber,
expectedDate: "2025-02-15",
reason: "Weather delay",
},
});
```
The `QueuedUserNotificationClient` validates the payload against the Zod schema, generates a deterministic `messageGroupId` from the recipient list, and publishes to the notification queue.
## 5. Create the React Email template
[Section titled “5. Create the React Email template”](#5-create-the-react-email-template)
Add a new template in `backend/services/email-smtp/src/emails/`:
backend/services/email-smtp/src/emails/shipment-delayed.tsx
```tsx
import type { UserShipmentDelayedNotification } from "#src/generated/notifications.ts";
import { Base, H1 } from "../components";
import { useTranslations } from "../i18n";
type Props = {
locale: string;
variables: UserShipmentDelayedNotification["variables"];
};
const ShipmentDelayed = ({ locale, variables }: Props) => {
const t = useTranslations("ShipmentDelayed", locale);
return (
{t("title")}
{/* Template body */}
);
};
ShipmentDelayed.PreviewProps = {
locale: "nl-NL",
variables: {
orderNumber: "ORD-2025-001",
expectedDate: "2025-02-15",
reason: "Weather delay",
},
};
export default ShipmentDelayed;
```
Templates use `@react-email/components` for cross-client HTML email rendering. The `render()` function produces both HTML and plain-text versions in parallel.
## 6. Add translations
[Section titled “6. Add translations”](#6-add-translations)
Add translation keys for every supported locale. Messages use ICU MessageFormat syntax for interpolation and plurals.
backend/services/email-smtp/src/messages/en-GB.json
```json
{
"ShipmentDelayed": {
"preview": "Shipment delayed for order #{orderNumber}",
"subject": "Your shipment has been delayed",
"title": "Shipment Delayed",
"body": "Your order {orderNumber} is now expected on {expectedDate}."
}
}
```
Repeat for all supported locales: `de-DE`, `en-GB`, `es-ES`, `fr-BE`, `fr-FR`, `it-IT`, `nl-BE`, `nl-NL`.
## 7. Register the handler
[Section titled “7. Register the handler”](#7-register-the-handler)
Add a case to the switch in `backend/services/email-smtp/src/handlers/index.ts`:
```typescript
case "shipment-delayed":
await handleShipmentDelayed(event);
break;
```
Then create the handler function:
backend/services/email-smtp/src/handlers/shipment-delayed.ts
```typescript
import ShipmentDelayed from "../emails/shipment-delayed";
import { useTranslations } from "../i18n";
import { render } from "#src/render.ts";
import { mapRecipients } from "#src/utils.ts";
import { sendMail } from "#src/mailer.ts";
import type { UserShipmentDelayedNotification } from "#src/generated/notifications.ts";
export const handleShipmentDelayed = async (
data: UserShipmentDelayedNotification,
): Promise => {
const t = useTranslations("ShipmentDelayed", data.storeContext.locale);
const { text, html } = await render(
ShipmentDelayed({
locale: data.storeContext.locale,
variables: data.variables,
}),
);
await sendMail({
recipients: mapRecipients(data.recipients),
subject: t("subject"),
html,
text,
});
};
```
## 8. Subscribe the queue (per cloud)
[Section titled “8. Subscribe the queue (per cloud)”](#8-subscribe-the-queue-per-cloud)
If your notification type uses a new event name, you need to add a subscription in the cloud function handler. The email service has separate entry points per cloud provider:
| Cloud | Handler location |
| ----- | --------------------------------------------------------- |
| AWS | `email-smtp/src/functions/aws/event-handler.ts` |
| GCP | `email-smtp/src/functions/gcp/event-handler.ts` |
| Azure | `email-smtp/src/functions/azure/internal-events/index.ts` |
All handlers call `processNotificationEvent(data)`. The subscription routing happens at the infrastructure level (EventBridge rules, Pub/Sub subscriptions, or Service Bus topics).
## 9. Test locally with Mailpit
[Section titled “9. Test locally with Mailpit”](#9-test-locally-with-mailpit)
With `task docker:services` running, Mailpit captures all SMTP traffic on `localhost:1025`. Open the Mailpit web UI at [localhost:8025](http://localhost:8025) to inspect rendered emails.
The `EMAIL_ALLOWLIST` variable filters recipients in non-production environments. Set it to your domain (e.g. `@labdigital.nl`) to prevent accidental delivery to real customers.
To trigger the notification locally, publish a test event through the notification client or call the handler directly in a test:
```typescript
await processNotificationEvent({
type: "shipment-delayed",
recipients: [{ email: "test@labdigital.nl", name: "Test User" }],
storeContext: { storeKey: "main-store", locale: "en-GB" },
variables: {
orderNumber: "ORD-2025-001",
expectedDate: "2025-02-15",
},
});
```
# Configuring Datadog with OpenTelemetry
> How to configure Datadog as the observability backend for Evolve using OpenTelemetry.
Evolve services export traces and metrics through OpenTelemetry (OTel) to an OTel Collector or directly to a backend. Datadog is OTel-compatible and works without changes to service code. This guide covers configuring the Collector exporter, environment variables, and Terraform secrets.
1. **Configure the OTel Collector exporter**
Add a Datadog exporter to your OpenTelemetry Collector configuration. The local development setup in `docker/otel-collector/otel-collector-config.yaml` ships traces to Jaeger by default. For Datadog, add or replace the exporter:
otel-collector-config.yaml
```yaml
exporters:
datadog:
api:
site: datadoghq.eu # or datadoghq.com for US
key: ${DD_API_KEY}
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [datadog]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [datadog]
```
The `otlp` receiver accepts data from services on gRPC (port 4317) and HTTP (port 4318). No changes to service code are required.
2. **Set environment variables**
Each service needs to know where to send telemetry. These variables are set in the service’s Terraform or container configuration:
```bash
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
OTEL_EXPORTER_OTLP_HEADERS=dd-api-key=
OTEL_SERVICE_NAME=catalog-commercetools
```
The `OTEL_SERVICE_NAME` maps to the Datadog service name in APM. Use the same name as your component for consistency. The `OTEL_EXPORTER_OTLP_HEADERS` variable passes authentication headers to the Collector or directly to Datadog’s OTLP endpoint.
Services initialize OTel through `initObservability()` in their `run.ts` entry point. This function configures the OpenTelemetry SDK, sets up the OTLP exporter (gRPC or HTTP based on `OTEL_EXPORTER_OTLP_PROTOCOL`), and optionally integrates with Sentry for error tracking.
3. **Configure Mach Composer**
Add the Datadog API key to your SOPS-encrypted secrets file:
```yaml
# config//tst.secrets.yaml
secrets:
datadog:
api_key: # encrypted by SOPS
```
Then reference the secret in your Mach Composer environment config and pass it to all components as a global secret and variable:
```yaml
# config//tst.yaml
global:
variables:
otel_exporter_otlp_endpoint: "https://your-otel-collector-endpoint"
secrets:
datadog_api_key: ${var.secrets.datadog.api_key}
```
4. **Wire the Terraform variables**
Each service’s Terraform receives the secret through `var.secrets` and injects it as environment variables in `locals.tf`:
```hcl
locals {
env_vars = {
OTEL_EXPORTER_OTLP_ENDPOINT = var.variables.otel_exporter_otlp_endpoint
OTEL_EXPORTER_OTLP_HEADERS = "dd-api-key=${var.secrets.datadog_api_key}"
OTEL_SERVICE_NAME = local.service_name
}
}
```
Follow the existing patterns in `backend/services/catalog-commercetools/terraform/aws/locals.tf` (or the equivalent Azure/GCP directory) to see how other observability secrets are injected.
5. **Verify traces and metrics**
After deploying the updated Collector:
1. Open the Datadog APM page and look for your service names
2. Trigger a request through the storefront and check that a distributed trace appears spanning the gateway and backend services
3. Check the Metrics Explorer for OTel metrics (request duration, error rate)
If traces do not appear, check the Collector logs for export errors and verify that the API key and site are correct.
6. **Add Core Web Vitals (optional)**
Evolve’s frontend reports Core Web Vitals through OTel. These metrics flow through the same Collector pipeline, so they appear in Datadog automatically once the exporter is configured. You can build dashboards tracking LCP, FID, and CLS per page and per deployment.
## Further reading
[Section titled “Further reading”](#further-reading)
* [Observability](/architecture/cloud/observability/) for the full OTel architecture, local development setup, and Core Web Vitals monitoring
* [SaaS configuration](/architecture/cloud/saas-configuration/) for managing third-party service credentials through Terraform
# Deploying to Vercel
> Set up a Vercel project for Evolve and deploy the full stack, from one-time linking and environment setup to a single deploy command.
You can deploy the full Evolve stack to Vercel as one project: the storefront, the GraphQL gateway, and the domain services. This guide covers the one-time setup and the deploy command. For how the stack maps to Vercel, see [Hosting on Vercel](/architecture/cloud/vercel/).
## One-time setup
[Section titled “One-time setup”](#one-time-setup)
These steps configure the Vercel project once. They are not part of the deploy command itself.
1. **Link the repository to a Vercel project**
```bash
vercel link
```
This creates a `.vercel/` directory linking the working copy to a Vercel project.
2. **Provision Redis**
Add Upstash Redis through the Vercel Marketplace integration and connect it to the project for all environments. This sets `REDIS_URL`.
3. **Set the environment variables**
Configure the production and preview environments. Add variables individually with the Vercel CLI:
```bash
vercel env add CTP_CLIENT_ID production
```
Or bulk-sync the allowlisted variables from your local `.env` (pulled from 1Password) with the included helper:
```bash
node scripts/vercel-env-sync.ts
```
Run it with `--check` to report drift between `.env` and the Vercel project without writing anything.
4. **Add the registry token**
The build installs the private `@evolve-framework/*` packages, so add the registry token as a build-time secret:
```bash
vercel env add EVOLVE_NPM_TOKEN production preview
```
## Deploy
[Section titled “Deploy”](#deploy)
1. **Deploy the stack**
```bash
task vercel:deploy
```
This generates the gateway’s deploy-time supergraph and then runs `vercel deploy --prod`. Vercel builds the storefront natively, the gateway, and the domain services, and rolls them out as one atomic deployment.
Pushing to `main` deploys to production, and opening a pull request creates a full-stack preview deployment.
2. **Verify the deployment**
```bash
# storefront
curl -sf "$DEPLOY_URL/api/healthcheck"
# gateway, through the public rewrite
curl -sf "$DEPLOY_URL/graphql" \
-H 'content-type: application/json' \
--data '{"query":"{ __typename }"}'
```
The domain services are private and are not reachable directly.
## After a schema change
[Section titled “After a schema change”](#after-a-schema-change)
The gateway ships a composed supergraph as a build artifact. After changing a subgraph’s schema, regenerate it so the gateway build stays in sync:
```bash
task vercel:supergraph
```
## Local development
[Section titled “Local development”](#local-development)
The local [development workflow](/evolve/getting-started/) with `pnpm dev` is unchanged. To run the Vercel topology locally with binding environment variables injected, use `vercel dev -L` (requires Docker).
# Integrating an external pricing engine
> How to integrate an external pricing engine with the Evolve storefront.
Many enterprises have an existing pricing system (ERP, CPQ, or custom engine) that needs to feed prices into the storefront. Evolve’s federated GraphQL architecture lets you add an external price source as a subgraph that extends the `ProductVariant` type without modifying any existing service.
1. **Create a pricing subgraph**
Scaffold a new service following the [service creation guide](/how-to/creating-a-service/). The module extends `ProductVariant` with an external price field:
```typescript
import { AbstractModule } from "@evolve-framework/core";
import { gql } from "graphql-tag";
export class ExternalPricingModule extends AbstractModule {
typedefs = gql`
extend type ProductVariant @key(fields: "sku") {
sku: String! @external
externalPrice: Money @shareable
}
type Money {
centAmount: Int!
currencyCode: String!
}
`;
resolvers = {
ProductVariant: {
__resolveReference: resolveVariantPrice,
externalPrice: externalPriceResolver,
},
};
}
```
The `@key(fields: "sku")` and `@external` directives are Apollo Federation directives that tell the gateway how to resolve `ProductVariant` across subgraphs.
2. **Batch with a DataLoader**
External pricing APIs are often latency-sensitive. Use a DataLoader to batch multiple SKU lookups into a single API call:
```typescript
import DataLoader from "dataloader";
const createPriceLoader = (currency: string) =>
new DataLoader(async (skus) => {
const prices = await fetchPricesFromEngine(skus, currency);
return skus.map((sku) => prices.get(sku) ?? null);
});
```
Create the DataLoader per request in `context.ts` so each request gets its own batch window:
src/context.ts
```typescript
import {
readStoreContextFromRequest,
type ServerContext,
} from "@evolve-framework/core/service/graphql";
import type { YogaInitialContext } from "graphql-yoga";
export const createContext = async (
serverContext: ServerContext & YogaInitialContext,
) => {
const storeContext = readStoreContextFromRequest(serverContext.request);
return {
...serverContext,
storeContext,
priceLoader: createPriceLoader(storeContext.currency),
};
};
```
3. **Implement the resolver**
The resolver uses the DataLoader to look up the price for each variant:
```typescript
const externalPriceResolver = async (variant, _args, context) => {
return context.priceLoader.load(variant.sku);
};
```
4. **Add caching**
For prices that do not change every second, wrap the fetch call with the framework cache:
```typescript
import { cache } from "@evolve-framework/core/cache";
const fetchPricesFromEngine = async (skus, currency) => {
return cache.wrapFn({
key: `ext-prices:${currency}:${skus.join(",")}`,
fn: () => pricingApi.getPrices(skus, currency),
ttl: 300, // seconds
});
};
```
5. **Compose into the supergraph**
For local development, add the pricing service to the composition task in `backend/services/graphql-gateway/Taskfile.yaml` and recompose the local supergraph:
```bash
task -d backend/services/graphql-gateway supergraph
```
In production the router polls the supergraph from the Hive CDN; the new subgraph joins it through the service’s `hive_schema_publish` Terraform resource. Once composed, the supergraph schema includes the `externalPrice` field on every `ProductVariant`.
The frontend can then query external prices alongside regular product data in a single GraphQL request:
```graphql
query Product($slug: String!) {
product(slug: $slug) {
variants {
sku
price { centAmount currencyCode }
externalPrice { centAmount currencyCode }
}
}
}
```
## Further reading
[Section titled “Further reading”](#further-reading)
* [Data model](/architecture/core/datamodel/) for the vendor-independent schema and extension patterns
* [GraphQL federation](/architecture/core/graphql/) for how subgraphs compose into the supergraph
* [Customization](/architecture/core/framework/customization/) for extending existing types and modules
# Local HTTPS with Portless
> Run every Evolve service behind a stable *.localhost HTTPS hostname with system-trusted certs, so OAuth callbacks, secure cookies, and cross-subdomain editor sessions Just Work locally.
A handful of Evolve flows need real HTTPS in development:
* OAuth callbacks (CMS sign-in for the [authoring](/evolve/authoring/) editor).
* Anything that sets `Secure` cookies — browsers won’t store them on `http://localhost`.
* Cross-subdomain editor sessions: the chrome on the storefront probes the authoring service through a sibling subdomain, which requires both ends to live on the same registrable HTTPS domain for `SameSite=Lax` cookies to ride along.
Evolve uses [Portless](https://portless.sh/) as the local-HTTPS layer. It’s a tiny local reverse proxy that terminates TLS in front of each service and gives every workspace a stable hostname under `*.evolve.localhost`. Services keep running on plain HTTP — Portless fronts them.
## One-time setup
[Section titled “One-time setup”](#one-time-setup)
```bash
pnpm dev:setup-portless
```
That does three things:
1. **`portless trust`** — installs the Portless CA into your OS keychain so the certs it mints are system-trusted.
2. **`portless service install`** — registers Portless as a background service so the proxy is always running, no manual foreground process to babysit.
3. **`turbo run setup:portless`** — walks every workspace that declares a `setup:portless` script and registers its alias.
Out of the box that registers:
* `https://storefront.evolve.localhost` → `http://localhost:3000`
* `https://authoring.evolve.localhost` → `http://localhost:4007`
Restart your browser after the first setup
Chrome, Firefox, and Safari each cache the OS root-cert list at process start. Until you fully quit and reopen the browser, the freshly-trusted Portless CA isn’t in the cached list and the `*.localhost` pages will show as untrusted.
## Why the two-level `*.evolve.localhost`
[Section titled “Why the two-level \*.evolve.localhost”](#why-the-two-level-evolvelocalhost)
A flat `*.localhost` would work for individual services but breaks the authoring chrome’s cross-origin session probe. Browsers treat each `.localhost` label as a separate registrable domain, so a cookie set on `authoring.localhost` is not readable from `storefront.localhost` even with `SameSite=Lax`.
By giving every service a *third*-level hostname under a shared `evolve.localhost` parent, the storefront and the authoring service share a registrable domain. The editor session cookie travels `SameSite=Lax`-safe between them, and the chrome can see who’s signed in without falling back to `SameSite=None` (which itself requires HTTPS — chicken/egg).
## Running with HTTPS
[Section titled “Running with HTTPS”](#running-with-https)
```bash
pnpm dev:secure
```
That command is the same as `pnpm dev` plus a banner script that prints every active Portless route, so you don’t have to grep `package.json` to remember which hostname maps to which port.
```plaintext
┌──────────────────────────────────────────────────────────────┐
│ Portless routes │
├──────────────────────────────────────────────────────────────┤
│ https://storefront.evolve.localhost → http://localhost:3000 │
│ https://authoring.evolve.localhost → http://localhost:4007 │
└──────────────────────────────────────────────────────────────┘
```
Services themselves keep running on plain HTTP. Portless terminates TLS in front of them, so your Fastify and Next.js configs need no HTTPS-specific code paths.
## Adding a new service
[Section titled “Adding a new service”](#adding-a-new-service)
Add a `setup:portless` script to the new package’s `package.json`:
```json
{
"scripts": {
"setup:portless": "portless alias "
}
}
```
Then re-run `pnpm dev:setup-portless`. The banner script picks the new route up automatically — it walks every workspace looking for `setup:portless` scripts and parses the alias out of the invocation, so there’s no central registry to keep in sync.
A few conventions:
* Use a dotted name (`authoring.evolve`, `storefront.evolve`) so the result lives under the shared `evolve.localhost` registrable domain.
* Pick a port that matches the service’s `dev` script. Portless doesn’t start the service for you — it just proxies what’s already listening.
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
**Browser shows the cert as untrusted.** You haven’t restarted the browser since `pnpm dev:setup-portless`. Quit fully (Cmd-Q, not just the window) and reopen.
**`ERR_CONNECTION_REFUSED` from a Portless hostname.** Portless is listening on the HTTPS port but the upstream service isn’t running on its local port yet. Make sure `pnpm dev:secure` is up.
**Cookies still rejected with `SameSite=Lax`.** Check that both ends of the call use the same `*.evolve.localhost` parent. A service that registered itself directly under `*.localhost` won’t share a registrable domain with the rest.
**Portless service died.** `portless service status` reports the state; `portless service restart` recovers it without redoing the trust step.
## Migrating from `--experimental-https`
[Section titled “Migrating from --experimental-https”](#migrating-from---experimental-https)
Earlier Evolve setups used Next.js’s `--experimental-https` flag to get a single HTTPS frontend. That works for one service but doesn’t solve the cross-subdomain story (the authoring service stayed on plain HTTP, so secure cookies couldn’t cross between them). Switching to Portless removed the flag from the Next dev script in favor of a single proxy in front of every service. Drop `--experimental-https` when you upgrade — Next.js itself stays on plain HTTP.
# Optimizing deployments
As a codebase grows, so does the complexity of the deployment process. This document outlines some best practices for optimizing deployments as you run into bottlenecks.
## Terraform and Artifacts
[Section titled “Terraform and Artifacts”](#terraform-and-artifacts)
The default setup for Evolve Platform is that Mach Composer will search for referenced terraform code in the same repository as the config. In smaller codebases this reduces overhead and makes executing terraform locally easier to do, and causes little cognitive overhead.
However as a codebase grows so does the amount of terraform code, and components might be deployed from different git branches or tags. This means that mach composer needs to checkout the repository for each component, which can be slow.
To circumvent this issue, we recommend to use a CI/CD pipeline to build and store the terraform artifacts in a cloud storage environment, and have Mach Composer read from there instead. To achieve this several steps need to be taken:
1. Add an additional command to your `build.ts` file to zip the terraform code during build time:
```typescript
await buildScripts({
docker: async () => {
await buildDocker({
entryPoint: "./src/index.ts",
outdir: "dist/",
});
},
terraform: async () => {
await bundleTerraform({
archiveName: "dist/terraform.zip",
source: "terraform",
});
},
});
```
2. Push the `terraform.zip` to a cloud storage environment, such as AWS S3 or Google Cloud Storage. This can most easily be achieved as part of the CI/CD pipeline. For example:
```yaml
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials #v2.0.0
with:
role-to-assume:
aws-region: eu-central-1
- name: Upload
shell: bash
run: aws s3 cp components/my-service/dist/terraform.zip s3://my-service-terraform/${{ github.sha }}.zip
```
3. Update the Mach Composer configuration to read from the cloud storage environment:
```yaml
components:
- name: account-commercetools
source: s3://my-service-terraform/
```
Doing this will configure Mach Composer to download the source code instead, and largely reduce the `init` phase.
# GraphQL API reference
> Auto-generated reference for the Evolve GraphQL Federation gateway, covering every query, mutation, and type.
The Evolve GraphQL gateway composes the account, catalog, checkout, CMS, order, and quotes subgraphs into a single federated API. This reference is generated directly from the composed supergraph schema.
## Operations
[Section titled “Operations”](#operations)
* [Queries](/reference/graphql/queries/) — 28 read operations
* [Mutations](/reference/graphql/mutations/) — 43 write operations
## Types
[Section titled “Types”](#types)
* [Objects](/reference/graphql/objects/) — 184
* [Input objects](/reference/graphql/inputs/) — 26
* [Enums](/reference/graphql/enums/) — 31
* [Interfaces](/reference/graphql/interfaces/) — 8
* [Unions](/reference/graphql/unions/) — 44
* [Scalars](/reference/graphql/scalars/) — 10
Note
This section is generated from `supergraph.generated.graphql`. Don’t edit the files under `reference/graphql/` by hand — run `pnpm gen:graphql` to refresh.
# Enums
> GraphQL enums exposed by the Evolve gateway.
31 enums in the Evolve GraphQL schema.
## AuthenticationErrorCode
[Section titled “AuthenticationErrorCode”](#authenticationerrorcode)
| Value | Description |
| --------------------- | ------------------------------ |
| `ACCOUNT_INACTIVE` | Account is inactive or blocked |
| `INVALID_CREDENTIALS` | Invalid credentials provided |
## AuthoringPageKind
[Section titled “AuthoringPageKind”](#authoringpagekind)
Storefront route kind. Decides which URL helper the drawer uses.
| Value | Description |
| --------- | ----------- |
| `catalog` | |
| `content` | |
## AuthoringPageStatus
[Section titled “AuthoringPageStatus”](#authoringpagestatus)
Editor-visible publish state for a page.
| Value | Description |
| ----------- | ----------- |
| `changed` | |
| `draft` | |
| `published` | |
## AuthoringPublishStatus
[Section titled “AuthoringPublishStatus”](#authoringpublishstatus)
Terminal status of a bulk publish action.
| Value | Description |
| ----------- | ----------- |
| `failed` | |
| `succeeded` | |
| `timedOut` | |
## BreadcrumbType
[Section titled “BreadcrumbType”](#breadcrumbtype)
Represents different types of breadcrumbs in the navigation structure.
| Value | Description |
| ---------- | -------------------------------------------- |
| `CATEGORY` | Represents a category in the breadcrumb. |
| `CONTENT` | Represents a content page in the breadcrumb. |
| `PRODUCT` | Represents a product in the breadcrumb. |
## ButtonVariant
[Section titled “ButtonVariant”](#buttonvariant)
Defines different visual styles for buttons.
| Value | Description |
| ----------- | ----------------------- |
| `primary` | Primary button style. |
| `secondary` | Secondary button style. |
| `tertiary` | Tertiary button style. |
## CartErrorCode
[Section titled “CartErrorCode”](#carterrorcode)
Error codes for recoverable cart operation failures. These represent state or business conditions the client can act on (e.g. recreate the cart, refetch and retry), as opposed to malformed input which is reported as a ValidationError.
| Value | Description |
| ----------------------- | --------------------------------------------------------------------------------------------------- |
| `CART_NOT_FOUND` | The cart no longer exists, has expired, or belongs to another session. Create a new cart and retry. |
| `CART_VERSION_CONFLICT` | The cart was modified concurrently. Refetch the cart to get the latest version and retry. |
| `OUT_OF_STOCK` | One or more requested line items are out of stock. |
## CatalogBlockFormat
[Section titled “CatalogBlockFormat”](#catalogblockformat)
| Value | Description |
| ------- | ---------------------------------- |
| `card` | Card format for catalog blocks. |
| `round` | Rounded format for catalog blocks. |
## CategorySortOrder
[Section titled “CategorySortOrder”](#categorysortorder)
Available sorting options for category listings
| Value | Description |
| ---------------- | -------------------------------- |
| `nameAscending` | Sort by name in descending order |
| `nameDescending` | Sort by name in ascending order |
## CheckoutCompleteErrorCode
[Section titled “CheckoutCompleteErrorCode”](#checkoutcompleteerrorcode)
Error codes for checkout completion failures
| Value | Description |
| -------------------------- | --------------------------------------- |
| `INVALID_PAYMENT_METHOD` | The provided payment method is invalid |
| `ORDER_CREATION_FAILED` | Failed to create the order |
| `TRANSACTION_START_FAILED` | Failed to start the payment transaction |
## ClientDevice
[Section titled “ClientDevice”](#clientdevice)
Supported client device types for platform-specific features
| Value | Description |
| --------- | -------------------------- |
| `android` | Android mobile application |
| `ios` | iOS mobile application |
| `web` | Web browser client |
## ContentVersion
[Section titled “ContentVersion”](#contentversion)
Specifies different versions of content.
| Value | Description |
| ----------- | --------------------------------- |
| `draft` | Draft version of the content. |
| `published` | Published version of the content. |
## ImagePosition
[Section titled “ImagePosition”](#imageposition)
Specifies the position options for images.
| Value | Description |
| --------- | -------------------------------- |
| `compact` | Smaller height of the image. |
| `full` | Image used as a full background. |
| `left` | Image positioned on the left. |
| `right` | Image positioned on the right. |
## ImageStyle
[Section titled “ImageStyle”](#imagestyle)
Defines the visual style to be applied to an image.
| Value | Description |
| ------- | -------------------------- |
| `dark` | Dark style for the image. |
| `light` | Light style for the image. |
## NavigationMenuItemType
[Section titled “NavigationMenuItemType”](#navigationmenuitemtype)
| Value | Description |
| ------------- | ----------- |
| `pageCatalog` | |
| `pageContent` | |
## OrderStatus
[Section titled “OrderStatus”](#orderstatus)
| Value | Description |
| ----------------- | ---------------------------------------- |
| `Cancelled` | Order has been cancelled |
| `Delivered` | Order has been delivered to the customer |
| `InTransit` | Order is currently in transit |
| `Open` | Order is open and active |
| `PaymentDue` | Payment is due for the order |
| `PickupAvailable` | Order is available for pickup |
| `Problem` | There is a problem with the order |
| `Processing` | Order is currently being processed |
| `Returned` | Order has been returned |
## PageSortOrder
[Section titled “PageSortOrder”](#pagesortorder)
Specifies sorting options for pages.
| Value | Description |
| ---------------- | --------------------------------- |
| `nameAscending` | Sort by name in ascending order. |
| `nameDescending` | Sort by name in descending order. |
## PageType
[Section titled “PageType”](#pagetype)
Defines the different types of pages available in the system.
| Value | Description |
| --------- | -------------------------- |
| `CATALOG` | Represents a catalog page. |
| `CONTENT` | Represents a content page. |
## PasswordChangeErrorCode
[Section titled “PasswordChangeErrorCode”](#passwordchangeerrorcode)
| Value | Description |
| -------------------------- | ---------------------------------------------------- |
| `INVALID_CURRENT_PASSWORD` | The current password provided is incorrect |
| `PASSWORDS_CANT_MATCH` | The new password is the same as the current password |
## PasswordResetErrorCode
[Section titled “PasswordResetErrorCode”](#passwordreseterrorcode)
| Value | Description |
| ----------------- | ---------------------------------------------- |
| `INVALID_TOKEN` | The password reset token is invalid or expired |
| `TOKEN_NOT_FOUND` | The password reset token was not found |
## PaymentCreateErrorCode
[Section titled “PaymentCreateErrorCode”](#paymentcreateerrorcode)
Error codes for payment creation failures
| Value | Description |
| ------------------------- | -------------------------------------------- |
| `INVALID_PAYMENT_METHOD` | Invalid payment method or arguments provided |
| `ORDER_ID_REQUIRED` | Order ID is required for payment creation |
| `PAYMENT_CREATION_FAILED` | Failed to create payment with provider |
## PaymentTargetType
[Section titled “PaymentTargetType”](#paymenttargettype)
Target type for payment operations
| Value | Description |
| ------- | ------------------------ |
| `CART` | Payment targets a cart |
| `ORDER` | Payment targets an order |
## ProductAvailability
[Section titled “ProductAvailability”](#productavailability)
Product availability status indicating stock levels and purchase options
| Value | Description |
| -------------- | ------------------------------------------------------------------------------ |
| `BACKORDER` | The product is temporarily out of stock, but customers can still place orders. |
| `DISCONTINUED` | The product has been permanently discontinued and is no longer available for |
| `IN_STOCK` | The product is currently in stock and available for purchase. |
| `OUT_OF_STOCK` | The product is temporarily unavailable because it is out of stock. |
## ProductSortOrder
[Section titled “ProductSortOrder”](#productsortorder)
Available sorting options for product listings
| Value | Description |
| ----------------- | --------------------------------- |
| `nameAscending` | Sort by name in ascending order |
| `nameDescending` | Sort by name in descending order |
| `popularity` | Sort by popularity |
| `priceAscending` | Sort by price in ascending order |
| `priceDescending` | Sort by price in descending order |
## QuoteRequestStatus
[Section titled “QuoteRequestStatus”](#quoterequeststatus)
Status values for quote requests
| Value | Description |
| ----------- | ---------------------------------------- |
| `REQUESTED` | Quote has been requested by the customer |
## QuoteStatus
[Section titled “QuoteStatus”](#quotestatus)
Status values for quotes
| Value | Description |
| ----------- | ---------------------------------------- |
| `REQUESTED` | Quote has been requested by the customer |
## ResourceLinkType
[Section titled “ResourceLinkType”](#resourcelinktype)
Specifies different types of resource links.
| Value | Description |
| ------------- | ---------------------------------------------------------- |
| `application` | Link to a specific page or section within the application. |
| `asset` | Asset link. |
| `catalog` | Catalog page content link type. |
| `content` | Category content link type. |
| `external` | External URL link. |
| `internal` | Internal page link. |
| `product` | Product content link type. |
## RichTextRenderer
[Section titled “RichTextRenderer”](#richtextrenderer)
Specifies different renderers for rich text content.
| Value | Description |
| ------------ | ------------------------------ |
| `contentful` | Contentful rich text renderer. |
| `storyblok` | Storyblok rich text renderer. |
## Salutation
[Section titled “Salutation”](#salutation)
Defines the available options for a salutation, including gender-neutral options.
| Value | Description |
| ------ | ----------- |
| `MR` | |
| `MS` | |
| `MX` | |
| `NONE` | |
## TwoColumnsRatio
[Section titled “TwoColumnsRatio”](#twocolumnsratio)
Specifies the ratio options for two-column layouts.
| Value | Description |
| ---------- | ----------------------------------------------- |
| `equal` | Equal ratio (50/50) for two columns. |
| `leftBig` | Left column is bigger (60/40) for two columns. |
| `rightBig` | Right column is bigger (40/60) for two columns. |
## UspBlockFormat
[Section titled “UspBlockFormat”](#uspblockformat)
| Value | Description |
| ------------ | --------------------------- |
| `horizontal` | Horizontal format for USPs. |
| `vertical` | Vertical format for USPs. |
# Input objects
> GraphQL input objects exposed by the Evolve gateway.
26 input objects in the Evolve GraphQL schema.
## AddressInput
[Section titled “AddressInput”](#addressinput)
Represents the basic structure for inputting an address, supporting both personal and business addresses.
| Field | Type | Default | Description |
| ----------------------- | -------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------- |
| `additionalAddressInfo` | `String` | | Extra information regarding the address, such as building number, floor, or apartment number. |
| `city` | `String`! | | The city where the address is located. Required. |
| `company` | `String` | | The company name for business addresses. This field is required for business addresses. |
| `country` | `String`! | | The country where the address is located. Must be a valid 2-letter ISO country code. |
| `email` | `String` | | The email address for contact, optional for both personal and business addresses. Must be a valid email format. |
| `familyName` | `String` | | The family name (last name) of the individual. May be left empty for company addresses. |
| `givenName` | `String` | | The given name (first name) of the individual. May be left empty for company addresses. |
| `id` | `ID` | | The unique identifier for the address. |
| `phone` | `String` | | The phone number associated with this address, typically including the international code. Optional. |
| `postalCode` | `String`! | | The postal code or ZIP code for the address, depending on the country. Required. |
| `region` | `String` | | The region, state, or province for the address. Used to represent subdivisions of countries. Optional. |
| `salutation` | [Salutation](/reference/graphql/enums/#salutation) | | The salutation or title for the person associated with the address (e.g., Mr., Ms.). Optional. |
| `streetAddress` | `String` | | The unstructured full street address, including street name, number, and optional suffix. |
| `streetName` | `String` | | The name of the street where the address is located. Required. |
| `streetNumber` | `String` | | The number of the building on the street. This field represents the main address number. Required. |
| `streetNumberSuffix` | `String` | | Additional suffixes to the street number, often used in countries like the Netherlands. |
## ApplePayPaymentContextInput
[Section titled “ApplePayPaymentContextInput”](#applepaypaymentcontextinput)
Input for Apple Pay payment validation
| Field | Type | Default | Description |
| -------------------- | --------- | ------- | ----------------------------- |
| `displayName` | `String`! | | Display name for the merchant |
| `domainName` | `String`! | | Domain name for validation |
| `merchantIdentifier` | `String`! | | Merchant identifier |
## AuthoringFinalizeUploadedAssetInput
[Section titled “AuthoringFinalizeUploadedAssetInput”](#authoringfinalizeuploadedassetinput)
Input for `authoringFinalizeUploadedAsset`.
| Field | Type | Default | Description |
| ------------- | ------------ | ------- | -------------------------------------------------------------------- |
| `contentType` | `String`! | | MIME type of the uploaded bytes (e.g. `image/png`, `video/mp4`). |
| `description` | `String` | | Description / alt copy. Optional. |
| `fileName` | `String`! | | Desired filename in CMS (e.g. `tennisrackets-hero-1.png`). |
| `tags` | \[`String`!] | | Tags applied to the asset (e.g. \[“ai-generated”, “veo”]). Optional. |
| `title` | `String` | | Display title for the Contentful asset library. Optional. |
| `uploadId` | `ID`! | | `sys.id` from a successful Contentful upload service POST. The |
## AuthoringPreparePublishEntriesInput
[Section titled “AuthoringPreparePublishEntriesInput”](#authoringpreparepublishentriesinput)
Input for `authoringPreparePublishEntries`.
| Field | Type | Default | Description |
| ---------- | --------- | ------- | ----------------------------------------------------------------- |
| `entryIds` | \[`ID`!]! | | Entry ids to prepare. Typically the `entries[*].id` list returned |
## AuthoringPublishEntriesInput
[Section titled “AuthoringPublishEntriesInput”](#authoringpublishentriesinput)
Input for `authoringPublishEntries`.
| Field | Type | Default | Description |
| ---------- | --------- | ------- | ----------------------------------------------------------------- |
| `entryIds` | \[`ID`!]! | | Entry ids to publish. Typically the `entries[*].id` list returned |
## AuthoringPushPageInput
[Section titled “AuthoringPushPageInput”](#authoringpushpageinput)
Input for `authoringPushPage`.
| Field | Type | Default | Description |
| ---------- | --------- | ------- | --------------------------------------------------------- |
| `pageJson` | `String`! | | JSON-encoded `PageData` (gateway shape — `ContentPage` or |
## CartLineItemCreateInput
[Section titled “CartLineItemCreateInput”](#cartlineitemcreateinput)
Input for creating a new line item
| Field | Type | Default | Description |
| ---------- | --------- | ------- | -------------------------- |
| `quantity` | `Int`! | | Quantity to add |
| `sku` | `String`! | | SKU of the product variant |
## CartLineItemUpdateInput
[Section titled “CartLineItemUpdateInput”](#cartlineitemupdateinput)
Input for updating an existing line item
| Field | Type | Default | Description |
| ---------- | --------- | ------- | ------------------------------ |
| `id` | `String`! | | Line item ID to update |
| `quantity` | `Int`! | | New quantity for the line item |
## CartSetBillingAddressInput
[Section titled “CartSetBillingAddressInput”](#cartsetbillingaddressinput)
Input for setting billing address on cart
| Field | Type | Default | Description |
| --------- | ------------------------------------------------------- | ------- | ------------------------ |
| `address` | [AddressInput](/reference/graphql/inputs/#addressinput) | | Customer billing address |
## CartSetCustomerEmailInput
[Section titled “CartSetCustomerEmailInput”](#cartsetcustomeremailinput)
Input for setting customer email on cart
| Field | Type | Default | Description |
| ------- | -------- | ------- | -------------- |
| `email` | `String` | | Customer email |
## CartSetPaymentMethodInput
[Section titled “CartSetPaymentMethodInput”](#cartsetpaymentmethodinput)
Input for setting payment method on cart
| Field | Type | Default | Description |
| --------------- | ------------------------------------------------------------------- | ------- | ------------------------------------ |
| `paymentMethod` | [PaymentMethodInput](/reference/graphql/inputs/#paymentmethodinput) | | Selected payment method for the cart |
## CartSetShippingAddressInput
[Section titled “CartSetShippingAddressInput”](#cartsetshippingaddressinput)
Input for setting shipping address on cart
| Field | Type | Default | Description |
| --------- | ------------------------------------------------------- | ------- | ------------------------- |
| `address` | [AddressInput](/reference/graphql/inputs/#addressinput) | | Customer shipping address |
## CartSetShippingMethodInput
[Section titled “CartSetShippingMethodInput”](#cartsetshippingmethodinput)
Input for setting shipping method on cart
| Field | Type | Default | Description |
| ---------------- | --------------------------------------------------------------------- | ------- | ------------------------------------- |
| `shippingMethod` | [ShippingMethodInput](/reference/graphql/inputs/#shippingmethodinput) | | Selected shipping method for the cart |
## CartUpdateAction
[Section titled “CartUpdateAction”](#cartupdateaction)
Union of all cart update actions
| Field | Type | Default | Description |
| -------------------- | ------------------------------------------------------------------------------------- | ------- | -------------------- |
| `setBillingAddress` | [CartSetBillingAddressInput](/reference/graphql/inputs/#cartsetbillingaddressinput) | | Set billing address |
| `setCustomerEmail` | [CartSetCustomerEmailInput](/reference/graphql/inputs/#cartsetcustomeremailinput) | | Set customer email |
| `setPaymentMethod` | [CartSetPaymentMethodInput](/reference/graphql/inputs/#cartsetpaymentmethodinput) | | Set payment method |
| `setShippingAddress` | [CartSetShippingAddressInput](/reference/graphql/inputs/#cartsetshippingaddressinput) | | Set shipping address |
| `setShippingMethod` | [CartSetShippingMethodInput](/reference/graphql/inputs/#cartsetshippingmethodinput) | | Set shipping method |
## CustomerCreateInput
[Section titled “CustomerCreateInput”](#customercreateinput)
Input data required to create a new customer account
| Field | Type | Default | Description |
| ------------ | --------------------------------------------------------- | ------- | -------------------------------------- |
| `email` | [EmailAddress](/reference/graphql/scalars/#emailaddress)! | | Email address for the customer account |
| `familyName` | `String` | | Family name of the customer |
| `givenName` | `String` | | Given name of the customer |
| `password` | `String`! | | Password for the customer account |
| `salutation` | [Salutation](/reference/graphql/enums/#salutation) | | Optional salutation for the customer |
## FacetFilterInput
[Section titled “FacetFilterInput”](#facetfilterinput)
| Field | Type | Default | Description |
| ------------ | ------------ | ------- | --------------------------- |
| `key` | `String`! | | Unique key for the facet |
| `max` | `Float` | | Required for Range Facets |
| `min` | `Float` | | Required for Range Facets |
| `selections` | \[`String`!] | | Required for Options Facets |
| `toggle` | `Boolean` | | Required for Toggle Facets |
## FilterConditionInput
[Section titled “FilterConditionInput”](#filterconditioninput)
| Field | Type | Default | Description |
| ------- | --------- | ------- | ---------------------- |
| `key` | `String`! | | Filter condition key |
| `value` | `String`! | | Filter condition value |
## PaymentContextInput
[Section titled “PaymentContextInput”](#paymentcontextinput)
Input for payment context when completing checkout or creating a payment
| Field | Type | Default | Description |
| ------------ | -------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------- |
| `cancelUrl` | `String`! | | URL to redirect to on cancelled payment |
| `failureUrl` | `String`! | | URL to redirect to on failed payment |
| `method` | [PaymentMethodInput](/reference/graphql/inputs/#paymentmethodinput)! | | Payment method details |
| `orderId` | `String` | | Optional order ID to associate the payment with. Only relevant if we create |
| `successUrl` | `String`! | | URL to redirect to on successful payment |
## PaymentMethodArgumentInput
[Section titled “PaymentMethodArgumentInput”](#paymentmethodargumentinput)
Input for payment method argument key-value pairs
| Field | Type | Default | Description |
| ------- | --------- | ------- | -------------- |
| `key` | `String`! | | Argument key |
| `value` | `String`! | | Argument value |
## PaymentMethodInput
[Section titled “PaymentMethodInput”](#paymentmethodinput)
Input for selecting a payment method
| Field | Type | Default | Description |
| ------------ | --------------------------------------------------------------------------------------- | ------- | ------------------------------------------- |
| `arguments` | \[[PaymentMethodArgumentInput](/reference/graphql/inputs/#paymentmethodargumentinput)!] | | Additional arguments for the payment method |
| `internalId` | `String`! | | Internal ID of the payment method |
| `provider` | `String`! | | Payment provider name |
## QuoteRequestCreateArgsInput
[Section titled “QuoteRequestCreateArgsInput”](#quoterequestcreateargsinput)
Input data for creating a new quote request
| Field | Type | Default | Description |
| --------------------- | -------- | ------- | ---------------------------------- |
| `comment` | `String` | | Optional comment from the customer |
| `purchaseOrderNumber` | `String` | | Optional purchase order number |
## ShippingMethodInput
[Section titled “ShippingMethodInput”](#shippingmethodinput)
Input for selecting a shipping method
| Field | Type | Default | Description |
| ----- | --------- | ------- | ------------------------- |
| `id` | `String`! | | ID of the shipping method |
## ShoppingListInput
[Section titled “ShoppingListInput”](#shoppinglistinput)
Input data for creating or updating a shopping list
| Field | Type | Default | Description |
| ------------- | --------- | ------- | ------------------------------------------ |
| `description` | `String` | | Optional description for the shopping list |
| `name` | `String`! | | Name for the shopping list |
## ShoppingListLineItemAddInput
[Section titled “ShoppingListLineItemAddInput”](#shoppinglistlineitemaddinput)
Input data for adding a new item to a shopping list
| Field | Type | Default | Description |
| ---------- | --------- | ------- | ---------------------------------------------- |
| `quantity` | `Int` | | Quantity of the item (optional, defaults to 1) |
| `sku` | `String`! | | SKU of the product variant to add |
## ShoppingListLineItemUpdateInput
[Section titled “ShoppingListLineItemUpdateInput”](#shoppinglistlineitemupdateinput)
Input data for updating an existing item in a shopping list
| Field | Type | Default | Description |
| ---------- | ----- | ------- | ------------------------------ |
| `id` | `ID`! | | ID of the line item to update |
| `quantity` | `Int` | | New quantity for the line item |
## StoreContextInput
[Section titled “StoreContextInput”](#storecontextinput)
| Field | Type | Default | Description |
| ---------- | --------- | ------- | ---------------------------------------------------- |
| `currency` | `String`! | | The currency used in the store context. |
| `locale` | `String`! | | The locale of the store context. For example: en-US. |
| `storeKey` | `String`! | | The store key associated with the store context. |
# Interfaces
> GraphQL interfaces exposed by the Evolve gateway.
8 interfaces in the Evolve GraphQL schema.
## AbstractPage
[Section titled “AbstractPage”](#abstractpage)
An abstract interface representing common properties of different page types.
| Field | Type | Description |
| ----------------- | ---------------------------------------------------------------- | ----------------------------------------------- |
| `breadcrumbs` | \[[Breadcrumb](/reference/graphql/objects/#breadcrumb)!] | Breadcrumb trail for the page. |
| `id` | `ID`! | Unique identifier for the page. |
| `meta` | [PageMeta](/reference/graphql/objects/#pagemeta) | Page metadata for SEO and other purposes. |
| `name` | `String`! | Name of the page. |
| `path` | `String`! | URL path of the page. |
| `translatedPaths` | \[[TranslatedPath](/reference/graphql/objects/#translatedpath)!] | List of translated paths for different locales. |
## AbstractProductSpecificationAttribute
[Section titled “AbstractProductSpecificationAttribute”](#abstractproductspecificationattribute)
| Field | Type | Description |
| ------------- | --------- | ------------------------------------------------------------------------------------- |
| `description` | `String` | Optional descriptive information about the attribute, Localized to the current locale |
| `label` | `String`! | The label of the attribute, e.g. ‘Color’. Localized to the current locale |
| `name` | `String`! | The name of the attribute, e.g. ‘color’. Consistent across all locales |
## CartLineItem
[Section titled “CartLineItem”](#cartlineitem)
Interface for cart line items
| Field | Type | Description |
| -------------- | ----------------------------------------------------- | ----------------------------------------- |
| `id` | `ID`! | Unique identifier for the line item |
| `listPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The list price (MSRP) |
| `price` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The final price after applying discounts |
| `quantity` | `Int`! | Number of items in the line item |
| `regularPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The regular selling price on the platform |
## Error
[Section titled “Error”](#error)
| Field | Type | Description |
| --------- | --------- | ----------- |
| `message` | `String`! | |
## OrderLineItem
[Section titled “OrderLineItem”](#orderlineitem)
| Field | Type | Description |
| -------------- | ----------------------------------------------------- | --------------------------------------------------------------------------- |
| `id` | `ID`! | Unique identifier for the line item |
| `listPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | Represents the list price, also known as te Manufacturer’s Suggested Retail |
| `name` | `String`! | Name of the line item |
| `price` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The line-item is the final price after applying any additional discounts on |
| `quantity` | `Int`! | Number of items in the line item |
| `regularPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The regular selling price on the platform, potentially already discounted |
## PaymentMethodExtraFields
[Section titled “PaymentMethodExtraFields”](#paymentmethodextrafields)
| Field | Type | Description |
| ------ | --------- | ----------- |
| `type` | `String`! | |
## ProductVariantDimensionOption
[Section titled “ProductVariantDimensionOption”](#productvariantdimensionoption)
Interface for product variant dimension options with common properties
| Field | Type | Description |
| -------------- | -------------------------------------------------------------------- | ------------------------------------------- |
| `availability` | [ProductAvailability](/reference/graphql/enums/#productavailability) | Availability status of this option |
| `label` | `String`! | Display label for the option |
| `product` | [Product](/reference/graphql/objects/#product) | The product this option belongs to |
| `selected` | `Boolean`! | Whether this option is currently selected |
| `value` | `String`! | Value of the option |
| `variant` | [ProductVariant](/reference/graphql/objects/#productvariant) | The specific variant this option represents |
## QuoteLineItem
[Section titled “QuoteLineItem”](#quotelineitem)
Interface for quote line items
| Field | Type | Description |
| -------------- | ----------------------------------------------------- | ----------------------------------------- |
| `id` | `ID`! | Unique identifier for the line item |
| `listPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The list price (MSRP) |
| `name` | `String`! | Product name for the line item |
| `price` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The final price after applying discounts |
| `quantity` | `Int`! | Number of items |
| `regularPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The regular selling price on the platform |
# Mutations
> All write operations exposed by the Evolve GraphQL gateway.
All write operations exposed by the Evolve GraphQL gateway.
## businessUnitAddressCreate
[Section titled “businessUnitAddressCreate”](#businessunitaddresscreate)
```graphql
businessUnitAddressCreate(
address: AddressInput!
businessUnitId: ID!
): BusinessUnitAddressCreateResponse!
```
Add a new address to a business unit
Example:
```graphql
mutation {
businessUnitAddressCreate(
businessUnitId: "bu-123"
address: {
firstName: "Business"
lastName: "Contact"
streetName: "Business Street"
streetNumber: "100"
city: "Amsterdam"
postalCode: "1000AA"
country: "NL"
}
) {
businessUnit {
id
addresses {
id
firstName
lastName
city
}
}
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [BusinessUnitAddressCreateResponse](/reference/graphql/objects/#businessunitaddresscreateresponse)!
**Arguments**
* `address`: [AddressInput](/reference/graphql/inputs/#addressinput)!
* `businessUnitId`: `ID`!
## businessUnitAddressDelete
[Section titled “businessUnitAddressDelete”](#businessunitaddressdelete)
```graphql
businessUnitAddressDelete(
addressId: ID!
businessUnitId: ID!
): BusinessUnitAddressDeleteResponse!
```
Remove an address from a business unit
Example:
```graphql
mutation {
businessUnitAddressDelete(
businessUnitId: "bu-123"
addressId: "addr-456"
) {
businessUnit {
id
addresses {
id
city
}
}
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [BusinessUnitAddressDeleteResponse](/reference/graphql/objects/#businessunitaddressdeleteresponse)!
**Arguments**
* `addressId`: `ID`!
* `businessUnitId`: `ID`!
## businessUnitAddressUpdate
[Section titled “businessUnitAddressUpdate”](#businessunitaddressupdate)
```graphql
businessUnitAddressUpdate(
address: AddressInput!
addressId: ID!
businessUnitId: ID!
): BusinessUnitAddressUpdateResponse!
```
Update an existing address in a business unit
Example:
```graphql
mutation {
businessUnitAddressUpdate(
businessUnitId: "bu-123"
addressId: "addr-456"
address: {
firstName: "Updated Business"
lastName: "Contact"
streetName: "New Business Street"
streetNumber: "200"
city: "Rotterdam"
postalCode: "2000BB"
country: "NL"
}
) {
businessUnit {
id
addresses {
id
firstName
lastName
city
}
}
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [BusinessUnitAddressUpdateResponse](/reference/graphql/objects/#businessunitaddressupdateresponse)!
**Arguments**
* `address`: [AddressInput](/reference/graphql/inputs/#addressinput)!
* `addressId`: `ID`!
* `businessUnitId`: `ID`!
## businessUnitAssociateInvite
[Section titled “businessUnitAssociateInvite”](#businessunitassociateinvite)
```graphql
businessUnitAssociateInvite(
businessUnitId: ID!
email: String!
roleIDs: [ID!]!
): BusinessUnitAssociateInviteResponse!
```
Invite a new associate to the business unit with specified roles
Example:
```graphql
mutation {
businessUnitAssociateInvite(
businessUnitId: "bu-123"
email: "new.associate@company.com"
roleIDs: ["role-admin", "role-buyer"]
) {
businessUnit {
id
associates {
id
email
roles {
id
key
}
}
}
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [BusinessUnitAssociateInviteResponse](/reference/graphql/objects/#businessunitassociateinviteresponse)!
**Arguments**
* `businessUnitId`: `ID`!
* `email`: `String`!
* `roleIDs`: \[`ID`!]!
## businessUnitAssociateRemove
[Section titled “businessUnitAssociateRemove”](#businessunitassociateremove)
```graphql
businessUnitAssociateRemove(
businessUnitId: ID!
customerId: ID!
): BusinessUnitAssociateRemoveResponse!
```
Remove an associate from the business unit
Example:
```graphql
mutation {
businessUnitAssociateRemove(
businessUnitId: "bu-123"
customerId: "customer-456"
) {
businessUnit {
id
associates {
id
email
}
}
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [BusinessUnitAssociateRemoveResponse](/reference/graphql/objects/#businessunitassociateremoveresponse)!
**Arguments**
* `businessUnitId`: `ID`!
* `customerId`: `ID`!
## businessUnitAssociateRolesUpdate
[Section titled “businessUnitAssociateRolesUpdate”](#businessunitassociaterolesupdate)
```graphql
businessUnitAssociateRolesUpdate(
businessUnitId: ID!
customerId: ID!
roleIDs: [ID!]!
): BusinessUnitAssociateRolesUpdateResponse!
```
Update associate roles for an existing associate in the business unit Note: All current roles will be replaced with the provided roles
Example:
```graphql
mutation {
businessUnitAssociateRolesUpdate(
businessUnitId: "bu-123"
customerId: "customer-456"
roleIDs: ["role-buyer", "role-viewer"]
) {
businessUnit {
id
associates {
id
email
roles {
id
key
}
}
}
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [BusinessUnitAssociateRolesUpdateResponse](/reference/graphql/objects/#businessunitassociaterolesupdateresponse)!
**Arguments**
* `businessUnitId`: `ID`!
* `customerId`: `ID`!
* `roleIDs`: \[`ID`!]!
## createGuestSession
[Section titled “createGuestSession”](#createguestsession)
```graphql
createGuestSession: CreateGuestSessionResponse!
```
Create a guest session for anonymous users. This is useful for initializing a session before any cart or account operations. The @requiresSession(create: true) directive ensures the gateway creates a session if one doesn’t exist.
Example:
```graphql
mutation {
createGuestSession {
success
}
}
```
**Returns:** [CreateGuestSessionResponse](/reference/graphql/objects/#createguestsessionresponse)!
## customerAddressCreate
[Section titled “customerAddressCreate”](#customeraddresscreate)
```graphql
customerAddressCreate(
address: AddressInput!
): CustomerAddressCreateResponse!
```
Add a new address to the customer’s address book
Example:
```graphql
mutation {
customerAddressCreate(address: {
firstName: "John"
lastName: "Doe"
streetName: "Secondary Street"
streetNumber: "456"
city: "Rotterdam"
postalCode: "2000BB"
country: "NL"
}) {
customer {
id
addresses {
id
firstName
lastName
city
}
}
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [CustomerAddressCreateResponse](/reference/graphql/objects/#customeraddresscreateresponse)!
**Arguments**
* `address`: [AddressInput](/reference/graphql/inputs/#addressinput)!
## customerAddressDelete
[Section titled “customerAddressDelete”](#customeraddressdelete)
```graphql
customerAddressDelete(
addressId: ID!
): CustomerAddressDeleteResponse!
```
Remove an address from the customer’s address book
Example:
```graphql
mutation {
customerAddressDelete(addressId: "addr-123") {
customer {
id
addresses {
id
city
}
}
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [CustomerAddressDeleteResponse](/reference/graphql/objects/#customeraddressdeleteresponse)!
**Arguments**
* `addressId`: `ID`!
## customerAddressUpdate
[Section titled “customerAddressUpdate”](#customeraddressupdate)
```graphql
customerAddressUpdate(
address: AddressInput!
addressId: ID!
): CustomerAddressUpdateResponse!
```
Update an existing address in the customer’s address book
Example:
```graphql
mutation {
customerAddressUpdate(
addressId: "addr-123"
address: {
firstName: "John"
lastName: "Doe"
streetName: "Updated Street"
streetNumber: "789"
city: "Utrecht"
postalCode: "3000CC"
country: "NL"
}
) {
customer {
id
addresses {
id
firstName
lastName
city
}
}
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [CustomerAddressUpdateResponse](/reference/graphql/objects/#customeraddressupdateresponse)!
**Arguments**
* `address`: [AddressInput](/reference/graphql/inputs/#addressinput)!
* `addressId`: `ID`!
## customerCreate
[Section titled “customerCreate”](#customercreate)
```graphql
customerCreate(
customer: CustomerCreateInput!
): CustomerCreateResponse!
```
Create a new customer account with email and password
Example:
```graphql
mutation {
customerCreate(customer: {
email: "john.doe@example.com"
password: "securePassword123"
givenName: "John"
familyName: "Doe"
salutation: MR
}) {
id
email
givenName
familyName
}
}
```
**Returns:** [CustomerCreateResponse](/reference/graphql/objects/#customercreateresponse)!
**Arguments**
* `customer`: [CustomerCreateInput](/reference/graphql/inputs/#customercreateinput)!
## customerDelete
[Section titled “customerDelete”](#customerdelete)
```graphql
customerDelete(
password: String!
): CustomerDeleteResponse!
```
Delete the current customer account (requires password confirmation)
Example:
```graphql
mutation {
customerDelete(password: "currentPassword123") {
success
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [CustomerDeleteResponse](/reference/graphql/objects/#customerdeleteresponse)!
**Arguments**
* `password`: `String`!
## customerEmailUpdate
[Section titled “customerEmailUpdate”](#customeremailupdate)
```graphql
customerEmailUpdate(
email: EmailAddress!
password: String!
): CustomerEmailUpdateResponse!
```
Update customer email address (requires password for verification)
Example:
```graphql
mutation {
customerEmailUpdate(
email: "newemail@example.com"
password: "currentPassword123"
) {
customer {
id
email
}
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [CustomerEmailUpdateResponse](/reference/graphql/objects/#customeremailupdateresponse)!
**Arguments**
* `email`: [EmailAddress](/reference/graphql/scalars/#emailaddress)!
* `password`: `String`!
## customerInformationUpdate
[Section titled “customerInformationUpdate”](#customerinformationupdate)
```graphql
customerInformationUpdate(
billingAddress: AddressInput!
): CustomerInformationUpdateResponse!
```
Update customer billing address information
Example:
```graphql
mutation {
customerInformationUpdate(billingAddress: {
firstName: "John"
lastName: "Doe"
streetName: "Main Street"
streetNumber: "123"
city: "Amsterdam"
postalCode: "1000AA"
country: "NL"
}) {
customer {
id
billingAddress {
firstName
lastName
city
}
}
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [CustomerInformationUpdateResponse](/reference/graphql/objects/#customerinformationupdateresponse)!
**Arguments**
* `billingAddress`: [AddressInput](/reference/graphql/inputs/#addressinput)!
## customerLogin
[Section titled “customerLogin”](#customerlogin)
```graphql
customerLogin(
email: EmailAddress!
password: String!
): CustomerLoginResponse!
```
Authenticate a customer with email and password
Example:
```graphql
mutation {
customerLogin(
email: "john.doe@example.com"
password: "securePassword123"
) {
id
email
givenName
familyName
}
}
```
**Returns:** [CustomerLoginResponse](/reference/graphql/objects/#customerloginresponse)!
**Arguments**
* `email`: [EmailAddress](/reference/graphql/scalars/#emailaddress)!
* `password`: `String`!
## customerLogout
[Section titled “customerLogout”](#customerlogout)
```graphql
customerLogout: CustomerLogoutResponse!
```
Log out the current customer and invalidate the session
Example:
```graphql
mutation {
customerLogout {
success
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [CustomerLogoutResponse](/reference/graphql/objects/#customerlogoutresponse)!
## customerPasswordReset
[Section titled “customerPasswordReset”](#customerpasswordreset)
```graphql
customerPasswordReset(
password: String!
token: String!
): CustomerPasswordResetResponse!
```
Reset customer password using a reset token received via email
Example:
```graphql
mutation {
customerPasswordReset(
token: "reset-token-from-email"
password: "newSecurePassword789"
) {
customer {
id
email
}
errors {
... on PasswordResetError {
code
message
}
... on ValidationError {
message
}
}
}
}
```
**Returns:** [CustomerPasswordResetResponse](/reference/graphql/objects/#customerpasswordresetresponse)!
**Arguments**
* `password`: `String`!
* `token`: `String`!
## customerPasswordResetRequest
[Section titled “customerPasswordResetRequest”](#customerpasswordresetrequest)
```graphql
customerPasswordResetRequest(
email: EmailAddress!
): CustomerPasswordResetRequestResponse!
```
Request a password reset email for the given email address
Example:
```graphql
mutation {
customerPasswordResetRequest(email: "john.doe@example.com")
}
```
**Returns:** [CustomerPasswordResetRequestResponse](/reference/graphql/objects/#customerpasswordresetrequestresponse)!
**Arguments**
* `email`: [EmailAddress](/reference/graphql/scalars/#emailaddress)!
## customerPasswordUpdate
[Section titled “customerPasswordUpdate”](#customerpasswordupdate)
```graphql
customerPasswordUpdate(
newPassword: String!
password: String!
): CustomerPasswordUpdateResponse!
```
Update customer password (requires current password for verification)
Example:
```graphql
mutation {
customerPasswordUpdate(
password: "currentPassword123"
newPassword: "newSecurePassword456"
) {
id
email
}
}
```
**Returns:** [CustomerPasswordUpdateResponse](/reference/graphql/objects/#customerpasswordupdateresponse)!
**Arguments**
* `newPassword`: `String`!
* `password`: `String`!
## refreshToken
[Section titled “refreshToken”](#refreshtoken)
```graphql
refreshToken: RefreshTokenResponse!
```
Refresh the authentication token for the current session
Example:
```graphql
mutation {
refreshToken {
success
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [RefreshTokenResponse](/reference/graphql/objects/#refreshtokenresponse)!
## setActiveBusinessContext
[Section titled “setActiveBusinessContext”](#setactivebusinesscontext)
```graphql
setActiveBusinessContext(
businessUnitId: ID!
storeKey: String!
): SetActiveBusinessContextResponse!
```
Set the active business context for B2B operations
Example:
```graphql
mutation {
setActiveBusinessContext(
businessUnitId: "bu-123"
storeKey: "nl-store"
) {
success
errors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [SetActiveBusinessContextResponse](/reference/graphql/objects/#setactivebusinesscontextresponse)!
**Arguments**
* `businessUnitId`: `ID`!
* `storeKey`: `String`!
## shoppingListCreate
[Section titled “shoppingListCreate”](#shoppinglistcreate)
```graphql
shoppingListCreate(
shoppingList: ShoppingListInput!
): ShoppingListCreateResponse!
```
Create a new shopping list for the current user
Example:
```graphql
mutation {
shoppingListCreate(shoppingList: {
name: "Office Supplies"
description: "Items needed for the office"
}) {
shoppingList {
id
name
description
lineItems {
id
sku
quantity
}
}
}
}
```
**Returns:** [ShoppingListCreateResponse](/reference/graphql/objects/#shoppinglistcreateresponse)!
**Arguments**
* `shoppingList`: [ShoppingListInput](/reference/graphql/inputs/#shoppinglistinput)!
## shoppingListDelete
[Section titled “shoppingListDelete”](#shoppinglistdelete)
```graphql
shoppingListDelete(
id: ID!
): ShoppingListDeleteResponse!
```
Delete a shopping list permanently
Example:
```graphql
mutation {
shoppingListDelete(id: "sl-123") {
shoppingList {
id
name
}
}
}
```
**Returns:** [ShoppingListDeleteResponse](/reference/graphql/objects/#shoppinglistdeleteresponse)!
**Arguments**
* `id`: `ID`!
## shoppingListLineItemsAdd
[Section titled “shoppingListLineItemsAdd”](#shoppinglistlineitemsadd)
```graphql
shoppingListLineItemsAdd(
id: ID
items: [ShoppingListLineItemAddInput!]!
): ShoppingListLineItemAddResponse!
```
Add products to a shopping list Note: “id” is optional for B2C/anonymous users - if not provided, a new list is created
Example:
```graphql
mutation {
shoppingListLineItemsAdd(
id: "sl-123"
items: [
{
sku: "SKU-001"
quantity: 2
},
{
sku: "SKU-002"
quantity: 1
}
]
) {
shoppingList
id
lineItems
id
sku
quantity
}
}
```
**Returns:** [ShoppingListLineItemAddResponse](/reference/graphql/objects/#shoppinglistlineitemaddresponse)!
**Arguments**
* `id`: `ID`
* `items`: \[[ShoppingListLineItemAddInput](/reference/graphql/inputs/#shoppinglistlineitemaddinput)!]!
## shoppingListLineItemsRemove
[Section titled “shoppingListLineItemsRemove”](#shoppinglistlineitemsremove)
```graphql
shoppingListLineItemsRemove(
id: ID!
itemIds: [String!]!
): ShoppingListLineItemRemoveResponse!
```
Remove specific items from a shopping list
Example:
```graphql
mutation {
shoppingListLineItemsRemove(
id: "sl-123"
itemIds: ["li-456", "li-789"]
) {
shoppingList {
id
lineItems {
id
sku
}
}
}
}
```
**Returns:** [ShoppingListLineItemRemoveResponse](/reference/graphql/objects/#shoppinglistlineitemremoveresponse)!
**Arguments**
* `id`: `ID`!
* `itemIds`: \[`String`!]!
## shoppingListLineItemsUpdate
[Section titled “shoppingListLineItemsUpdate”](#shoppinglistlineitemsupdate)
```graphql
shoppingListLineItemsUpdate(
id: ID!
items: [ShoppingListLineItemUpdateInput!]!
): ShoppingListLineItemUpdateResponse!
```
Update quantities or other properties of existing shopping list items
Example:
```graphql
mutation {
shoppingListLineItemsUpdate(
id: "sl-123"
items: [
{
id: "li-456"
quantity: 5
},
{
id: "li-789"
quantity: 3
}
]
) {
shoppingList {
id
lineItems {
id
sku
quantity
}
}
```
**Returns:** [ShoppingListLineItemUpdateResponse](/reference/graphql/objects/#shoppinglistlineitemupdateresponse)!
**Arguments**
* `id`: `ID`!
* `items`: \[[ShoppingListLineItemUpdateInput](/reference/graphql/inputs/#shoppinglistlineitemupdateinput)!]!
## shoppingListUpdate
[Section titled “shoppingListUpdate”](#shoppinglistupdate)
```graphql
shoppingListUpdate(
id: ID!
shoppingList: ShoppingListInput!
): ShoppingListUpdateResponse!
```
Update an existing shopping list’s information
Example:
```graphql
mutation {
shoppingListUpdate(
id: "sl-123"
shoppingList: {
name: "Updated Office Supplies"
description: "Updated items needed for the office"
}
) {
shoppingList {
id
name
description
}
}
}
```
**Returns:** [ShoppingListUpdateResponse](/reference/graphql/objects/#shoppinglistupdateresponse)!
**Arguments**
* `id`: `ID`!
* `shoppingList`: [ShoppingListInput](/reference/graphql/inputs/#shoppinglistinput)!
## \_empty
[Section titled “\_empty”](#_empty)
```graphql
_empty: String
```
Placeholder query to satisfy schema requirements for federation
**Returns:** `String`
## cartDiscountCodeAdd
[Section titled “cartDiscountCodeAdd”](#cartdiscountcodeadd)
```graphql
cartDiscountCodeAdd(
code: String!
id: ID!
): CartDiscountCodeAddResponse!
```
Apply a discount code to the cart
Example:
```graphql
mutation {
cartDiscountCodeAdd(
id: "cart-123",
code: "SAVE10"
) {
id
version
discountCodes {
discountCode {
id
code
}
state
}
totalPrice {
value
currencyCode
}
}
}
```
**Returns:** [CartDiscountCodeAddResponse](/reference/graphql/objects/#cartdiscountcodeaddresponse)!
**Arguments**
* `code`: `String`!
* `id`: `ID`!
## cartDiscountCodeRemove
[Section titled “cartDiscountCodeRemove”](#cartdiscountcoderemove)
```graphql
cartDiscountCodeRemove(
discountCodeId: ID!
id: ID!
): CartDiscountCodeRemoveResponse!
```
Remove a discount code from the cart
Example:
```graphql
mutation {
cartDiscountCodeRemove(
id: "cart-123",
discountCodeId: "discount-456"
) {
id
version
discountCodes {
discountCode {
id
code
}
}
totalPrice {
value
currencyCode
}
}
}
```
**Returns:** [CartDiscountCodeRemoveResponse](/reference/graphql/objects/#cartdiscountcoderemoveresponse)!
**Arguments**
* `discountCodeId`: `ID`!
* `id`: `ID`!
## cartLineItemsAdd
[Section titled “cartLineItemsAdd”](#cartlineitemsadd)
```graphql
cartLineItemsAdd(
id: ID
lineItems: [CartLineItemCreateInput!]!
): CartLineItemsAddResponse!
```
Add line items to cart or create a new cart
**Returns:** [CartLineItemsAddResponse](/reference/graphql/objects/#cartlineitemsaddresponse)!
**Arguments**
* `id`: `ID`
* `lineItems`: \[[CartLineItemCreateInput](/reference/graphql/inputs/#cartlineitemcreateinput)!]!
## cartLineItemsRemove
[Section titled “cartLineItemsRemove”](#cartlineitemsremove)
```graphql
cartLineItemsRemove(
id: ID!
lineItemIds: [String!]!
): CartLineItemsRemoveResponse!
```
Remove line items from the cart
**Returns:** [CartLineItemsRemoveResponse](/reference/graphql/objects/#cartlineitemsremoveresponse)!
**Arguments**
* `id`: `ID`!
* `lineItemIds`: \[`String`!]!
## cartLineItemsUpdate
[Section titled “cartLineItemsUpdate”](#cartlineitemsupdate)
```graphql
cartLineItemsUpdate(
id: ID!
lineItems: [CartLineItemUpdateInput!]!
): CartLineItemsUpdateResponse!
```
Update quantities of existing line items
**Returns:** [CartLineItemsUpdateResponse](/reference/graphql/objects/#cartlineitemsupdateresponse)!
**Arguments**
* `id`: `ID`!
* `lineItems`: \[[CartLineItemUpdateInput](/reference/graphql/inputs/#cartlineitemupdateinput)!]!
## cartUpdate
[Section titled “cartUpdate”](#cartupdate)
```graphql
cartUpdate(
actions: [CartUpdateAction!]!
id: ID!
): CartUpdateResponse!
```
Update cart with various actions
**Returns:** [CartUpdateResponse](/reference/graphql/objects/#cartupdateresponse)!
**Arguments**
* `actions`: \[[CartUpdateAction](/reference/graphql/inputs/#cartupdateaction)!]!
* `id`: `ID`!
## checkoutComplete
[Section titled “checkoutComplete”](#checkoutcomplete)
```graphql
checkoutComplete(
id: ID!
paymentContext: PaymentContextInput!
): CheckoutCompleteResponse!
```
Complete the checkout process, create an order, and initiate payment
**Returns:** [CheckoutCompleteResponse](/reference/graphql/objects/#checkoutcompleteresponse)!
**Arguments**
* `id`: `ID`! — Cart ID to complete checkout for
* `paymentContext`: [PaymentContextInput](/reference/graphql/inputs/#paymentcontextinput)! — Payment context for initiating payment
## createPayment
[Section titled “createPayment”](#createpayment)
```graphql
createPayment(
paymentContext: PaymentContextInput!
): CreatePaymentResponse!
```
Create a payment for an order or cart
Example:
```graphql
mutation {
createPayment(paymentContext: {
method: {
internalId: "pm-123"
provider: "stripe"
}
successUrl: "https://mystore.com/checkout/success"
failureUrl: "https://mystore.com/checkout/failure"
}) {
payment {
redirectURL
}
paymentErrors {
... on PaymentCreateError {
code
message
}
}
}
}
```
**Returns:** [CreatePaymentResponse](/reference/graphql/objects/#createpaymentresponse)!
**Arguments**
* `paymentContext`: [PaymentContextInput](/reference/graphql/inputs/#paymentcontextinput)!
## validateApplePayPayment
[Section titled “validateApplePayPayment”](#validateapplepaypayment)
```graphql
validateApplePayPayment(
paymentContext: ApplePayPaymentContextInput!
): ValidateApplePayPaymentResponse!
```
Validate an Apple Pay payment method and retrieve payment data Used for Apple Pay payment processing integration
Example:
```graphql
mutation {
validateApplePayPayment(paymentContext: {
merchantIdentifier: "merchant.com.mystore"
domainName: "mystore.com"
displayName: "My Store Purchase"
}) {
data
applePayErrors {
... on ValidationError {
message
}
}
}
}
```
**Returns:** [ValidateApplePayPaymentResponse](/reference/graphql/objects/#validateapplepaypaymentresponse)!
**Arguments**
* `paymentContext`: [ApplePayPaymentContextInput](/reference/graphql/inputs/#applepaypaymentcontextinput)!
## authoringFinalizeUploadedAsset
[Section titled “authoringFinalizeUploadedAsset”](#authoringfinalizeuploadedasset)
```graphql
authoringFinalizeUploadedAsset(
input: AuthoringFinalizeUploadedAssetInput!
): AuthoringFinalizeUploadedAssetResult!
```
Finalise a streaming asset upload. The bytes themselves are streamed from the chrome through `authoring-ai` directly to Contentful’s upload service — they never round-trip through this mutation as a buffered payload. This call receives the resulting `uploadId` plus caller-supplied metadata, creates the Asset entry, runs `processForAllLocales`, polls until processing completes, then publishes. Returns the public CDN URL the chrome rewrites into PageData before calling `authoringPushPage`.
**Returns:** [AuthoringFinalizeUploadedAssetResult](/reference/graphql/objects/#authoringfinalizeuploadedassetresult)!
**Arguments**
* `input`: [AuthoringFinalizeUploadedAssetInput](/reference/graphql/inputs/#authoringfinalizeuploadedassetinput)!
## authoringPreparePublishEntries
[Section titled “authoringPreparePublishEntries”](#authoringpreparepublishentries)
```graphql
authoringPreparePublishEntries(
input: AuthoringPreparePublishEntriesInput!
): AuthoringPreparePublishEntriesResult!
```
Pre-publish validation + locale-backfill. Given the entry ids from a preceding `authoringPushPage`, fetches each entry, finds any required-localized field whose target locale is empty, and copies a value from any sibling locale that has one. Returns the list of fixes plus any deterministic-unfixable issues so the caller can halt before invoking `authoringPublishEntries`. No-op for entries that already validate; safe to chain on every workflow.
**Returns:** [AuthoringPreparePublishEntriesResult](/reference/graphql/objects/#authoringpreparepublishentriesresult)!
**Arguments**
* `input`: [AuthoringPreparePublishEntriesInput](/reference/graphql/inputs/#authoringpreparepublishentriesinput)!
## authoringPublishEntries
[Section titled “authoringPublishEntries”](#authoringpublishentries)
```graphql
authoringPublishEntries(
input: AuthoringPublishEntriesInput!
): AuthoringPublishEntriesResult!
```
Publish a set of entries via Contentful’s Bulk Action API. The bulk action handles dependency ordering automatically (children before parents), so passing the full list returned from `authoringPushPage` is enough — no need to topologically sort on the caller side. Blocks until the bulk action reaches a terminal status (succeeded / failed), polling Contentful every 1s with a 60s timeout. Use from a workflow step that wants to flip a `push-page` result to Published in one call.
**Returns:** [AuthoringPublishEntriesResult](/reference/graphql/objects/#authoringpublishentriesresult)!
**Arguments**
* `input`: [AuthoringPublishEntriesInput](/reference/graphql/inputs/#authoringpublishentriesinput)!
## authoringPushPage
[Section titled “authoringPushPage”](#authoringpushpage)
```graphql
authoringPushPage(
input: AuthoringPushPageInput!
): AuthoringPushResult!
```
Push an authored page (gateway-shape `PageData`) to the CMS. The editor sends the page as JSON; the subgraph flattens it into Contentful entries, diffs against existing entries (preserving ids to avoid duplicates), topologically sorts by dependency, and persists via Management API using the editor user’s OAuth token. Asset upload is not supported here — image references must already be Contentful CDN URLs (use `authoringSearchAssets` to pick from the library).
**Returns:** [AuthoringPushResult](/reference/graphql/objects/#authoringpushresult)!
**Arguments**
* `input`: [AuthoringPushPageInput](/reference/graphql/inputs/#authoringpushpageinput)!
## storeContentPreview
[Section titled “storeContentPreview”](#storecontentpreview)
```graphql
storeContentPreview(
data: String!
path: String!
): StoreContentPreviewResponse!
```
Store content preview data for a specific path to enable content previewing Used by CMS editors to preview unpublished content changes
Example:
```graphql
mutation {
storeContentPreview(
path: "/about-us"
data: "{"title":"About Us","content":[{"type":"text","value":"Updated content"}]}"
) {
id
}
}
```
**Returns:** [StoreContentPreviewResponse](/reference/graphql/objects/#storecontentpreviewresponse)!
**Arguments**
* `data`: `String`!
* `path`: `String`!
## quoteRequestCreate
[Section titled “quoteRequestCreate”](#quoterequestcreate)
```graphql
quoteRequestCreate(
args: QuoteRequestCreateArgsInput!
id: ID!
): QuoteRequestCreateResponse!
```
Create a quote request from a cart
**Returns:** [QuoteRequestCreateResponse](/reference/graphql/objects/#quoterequestcreateresponse)!
**Arguments**
* `args`: [QuoteRequestCreateArgsInput](/reference/graphql/inputs/#quoterequestcreateargsinput)!
* `id`: `ID`!
# Objects
> GraphQL objects exposed by the Evolve gateway.
184 objects in the Evolve GraphQL schema.
## Address
[Section titled “Address”](#address)
Represents the basic structure for an address, supporting both private and company addresses.
| Field | Type | Description |
| ----------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `additionalAddressInfo` | `String` | Extra information regarding the address, such as building number, floor, or apartment number. |
| `city` | `String` | The city where the address is located. |
| `company` | `String` | The company name for business addresses. This field may be omitted for personal addresses. |
| `country` | `String`! | The country where the address is located. Must be provided. |
| `email` | `String` | The email address for contact, optional for both personal and business addresses. |
| `familyName` | `String` | The family name (last name) of the individual. May be left empty for company addresses. |
| `givenName` | `String` | The given name (first name) of the individual. May be left empty for company addresses. |
| `id` | `ID` | |
| `phone` | `String` | The phone number associated with this address, typically including the international code. |
| `postalCode` | `String`! | The postal code or ZIP code for the address, depending on the country. |
| `region` | `String` | The region, state, or province for the address. Used to represent subdivisions of countries. |
| `salutation` | [Salutation](/reference/graphql/enums/#salutation) | The salutation or title for the person associated with the address. |
| `streetAddress` | `String` | The unstructured full street address, including street name, number, and optional suffix. |
| `streetName` | `String` | The name of the street where the address is located. |
| `streetNumber` | `String` | The number of the building on the street. This field represents the main address number. |
| `streetNumberSuffix` | `String` | Additional suffixes to the street number, often used in countries like the Netherlands. |
## AddressConnection
[Section titled “AddressConnection”](#addressconnection)
Paginated connection for addresses
| Field | Type | Description |
| ------------ | --------------------------------------------------- | -------------------------------------------- |
| `items` | \[[Address](/reference/graphql/objects/#address)!]! | List of address items in the current page |
| `totalCount` | `Int`! | Total number of addresses matching the query |
## AddressSuggestion
[Section titled “AddressSuggestion”](#addresssuggestion)
| Field | Type | Description |
| -------------------- | -------- | ----------- |
| `city` | `String` | |
| `country` | `String` | |
| `postalCode` | `String` | |
| `streetName` | `String` | |
| `streetNumber` | `String` | |
| `streetNumberSuffix` | `String` | |
## AddressValidationResult
[Section titled “AddressValidationResult”](#addressvalidationresult)
| Field | Type | Description |
| ------------- | --------------------------------------------------------------------- | ----------- |
| `suggestions` | \[[AddressSuggestion](/reference/graphql/objects/#addresssuggestion)] | |
| `valid` | `Boolean`! | |
## ApplePayExtra
[Section titled “ApplePayExtra”](#applepayextra)
**Implements:** [PaymentMethodExtraFields](/reference/graphql/interfaces/#paymentmethodextrafields)
| Field | Type | Description |
| --------------- | ---------------------------------------------------------------------------------------------------- | ----------- |
| `brands` | \[`String`!] | |
| `configuration` | [PaymentMethodApplePayConfiguration](/reference/graphql/objects/#paymentmethodapplepayconfiguration) | |
| `type` | `String`! | |
## AuthenticationError
[Section titled “AuthenticationError”](#authenticationerror)
**Implements:** [Error](/reference/graphql/interfaces/#error)
| Field | Type | Description |
| --------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `code` | [AuthenticationErrorCode](/reference/graphql/enums/#authenticationerrorcode)! | Error code indicating the type of authentication failure |
| `message` | `String`! | Human readable error message describing the authentication failure. Not intended for end users. |
## AuthoringAssetSearchItem
[Section titled “AuthoringAssetSearchItem”](#authoringassetsearchitem)
A single asset returned by `authoringSearchAssets`.
| Field | Type | Description |
| ------------- | --------- | ------------------------------------------- |
| `contentType` | `String` | MIME type, e.g. `image/jpeg`. |
| `description` | `String` | Optional description / alt text. |
| `height` | `Int` | Image height in pixels, if known. |
| `id` | `ID`! | Stable id of the asset (CMS-side entry id). |
| `title` | `String`! | Display title. |
| `updatedAt` | `String` | ISO timestamp of the asset’s last update. |
| `url` | `String` | CDN URL of the asset, if available. |
| `width` | `Int` | Image width in pixels, if known. |
## AuthoringAssetSearchResult
[Section titled “AuthoringAssetSearchResult”](#authoringassetsearchresult)
Paginated authoring asset search results.
| Field | Type | Description |
| --------- | ------------------------------------------------------------------------------------- | --------------------------------------------- |
| `results` | \[[AuthoringAssetSearchItem](/reference/graphql/objects/#authoringassetsearchitem)!]! | List of asset search items. |
| `total` | `Int`! | Total number of assets matching the criteria. |
## AuthoringEntrySearchItem
[Section titled “AuthoringEntrySearchItem”](#authoringentrysearchitem)
A single entry returned by `authoringSearchEntries`.
| Field | Type | Description |
| -------------- | --------- | -------------------------------------------------------------- |
| `contentType` | `String`! | CMS content-type id (e.g. `heroBlock`). |
| `id` | `ID`! | Stable id of the entry (CMS-side entry id). |
| `internalName` | `String` | Optional internal name (used by editors for disambiguation). |
| `title` | `String`! | Display title (best-effort across `title` / `label` / `name`). |
| `updatedAt` | `String` | ISO timestamp of the entry’s last update. |
## AuthoringEntrySearchResult
[Section titled “AuthoringEntrySearchResult”](#authoringentrysearchresult)
Paginated authoring entry search results.
| Field | Type | Description |
| --------- | ------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `results` | \[[AuthoringEntrySearchItem](/reference/graphql/objects/#authoringentrysearchitem)!]! | List of entry search items. |
| `total` | `Int`! | Total number of entries matching the criteria. |
## AuthoringFinalizeUploadedAssetResult
[Section titled “AuthoringFinalizeUploadedAssetResult”](#authoringfinalizeuploadedassetresult)
Result of a successful `authoringFinalizeUploadedAsset` mutation.
| Field | Type | Description |
| ------------- | --------- | -------------------------------------------------------- |
| `assetId` | `ID`! | Real Contentful Asset id. |
| `contentType` | `String`! | Echoed MIME type. |
| `url` | `String`! | Public CDN URL of the uploaded asset (after processing). |
## AuthoringPageSummary
[Section titled “AuthoringPageSummary”](#authoringpagesummary)
Per-page summary returned by `authoringListPages`.
| Field | Type | Description |
| ----------- | --------------------------------------------------------------------- | ------------------------------------------------------ |
| `author` | `String` | Last editor’s display name, or null if not resolvable. |
| `id` | `ID`! | |
| `kind` | [AuthoringPageKind](/reference/graphql/enums/#authoringpagekind)! | |
| `slug` | `String`! | |
| `status` | [AuthoringPageStatus](/reference/graphql/enums/#authoringpagestatus)! | |
| `title` | `String`! | |
| `updatedAt` | `String`! | ISO timestamp of the last edit. |
## AuthoringPreparePublishEntriesResult
[Section titled “AuthoringPreparePublishEntriesResult”](#authoringpreparepublishentriesresult)
Result of a `authoringPreparePublishEntries` pass.
| Field | Type | Description |
| ----------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `fixed` | \[[AuthoringPreparePublishFix](/reference/graphql/objects/#authoringpreparepublishfix)!]! | List of locale-backfill writes that were applied. |
| `ready` | \[`ID`!]! | Entry ids that are now publish-ready. Empty when `unfixable` is |
| `unfixable` | \[[AuthoringPreparePublishUnfixable](/reference/graphql/objects/#authoringpreparepublishunfixable)!]! | Validation issues backfill couldn’t resolve (e.g. enum mismatches, |
## AuthoringPreparePublishFix
[Section titled “AuthoringPreparePublishFix”](#authoringpreparepublishfix)
Describes a single locale-backfill write.
| Field | Type | Description |
| -------------- | --------- | -------------------------------------------------------------- |
| `entryId` | `ID`! | Entry the fix was applied to. |
| `field` | `String`! | Field id (e.g. `name`, `slug`). |
| `locale` | `String`! | Locale code that was missing and is now filled (e.g. `en-GB`). |
| `sourceLocale` | `String`! | Locale code we copied the value from (e.g. `nl-NL`). |
## AuthoringPreparePublishUnfixable
[Section titled “AuthoringPreparePublishUnfixable”](#authoringpreparepublishunfixable)
Describes a validation issue that deterministic backfill can’t resolve.
| Field | Type | Description |
| ---------- | --------- | ------------------------------------------------------------------------- |
| `entryId` | `ID`! | Entry the issue belongs to. |
| `entryUrl` | `String`! | Direct URL to open the entry in the Contentful web app for manual repair. |
| `field` | `String`! | Field id with the problem. |
| `reason` | `String`! | Human-readable reason — surfaced verbatim back to the editor. |
## AuthoringPublishEntriesResult
[Section titled “AuthoringPublishEntriesResult”](#authoringpublishentriesresult)
Result of an `authoringPublishEntries` bulk publish.
| Field | Type | Description |
| -------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `bulkActionId` | `ID`! | Contentful bulk-action id, useful for cross-referencing in CF logs. |
| `errors` | \[[AuthoringPublishEntryError](/reference/graphql/objects/#authoringpublishentryerror)!]! | Per-entry error messages, if any. Empty on full success. |
| `failed` | `Int`! | Number of entries that failed to publish. |
| `published` | `Int`! | Number of entries published successfully. |
| `status` | [AuthoringPublishStatus](/reference/graphql/enums/#authoringpublishstatus)! | Terminal status reported by Contentful’s bulk action. |
## AuthoringPublishEntryError
[Section titled “AuthoringPublishEntryError”](#authoringpublishentryerror)
Error against a single entry within a bulk publish.
| Field | Type | Description |
| --------- | --------- | ------------------------------------------------------------ |
| `entryId` | `ID`! | Real Contentful entry id. |
| `message` | `String`! | Error message string (CF’s error code + details, truncated). |
## AuthoringPushedEntry
[Section titled “AuthoringPushedEntry”](#authoringpushedentry)
Per-entry summary returned by `authoringPushPage`.
| Field | Type | Description |
| ------------- | --------- | ------------------------------------------------------- |
| `contentType` | `String`! | Contentful content type id (e.g. `hero`, `productRow`). |
| `id` | `ID`! | Real Contentful entry id. |
| `title` | `String`! | Human-readable title used in the Contentful UI. |
## AuthoringPushLocales
[Section titled “AuthoringPushLocales”](#authoringpushlocales)
Locales used by the push.
| Field | Type | Description |
| --------- | --------- | ------------------------------------------------------------------ |
| `default` | `String`! | Contentful space default locale (used for non-localized fields). |
| `request` | `String`! | Storefront request locale (used for localized fields like `slug`). |
## AuthoringPushResult
[Section titled “AuthoringPushResult”](#authoringpushresult)
Result of a successful `authoringPushPage` mutation.
| Field | Type | Description |
| --------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `canonicalPath` | `String`! | Canonical page slug as written to CF (locale prefix stripped, leading |
| `contentfulUrl` | `String`! | Direct URL to open the page in the Contentful web app. |
| `entries` | \[[AuthoringPushedEntry](/reference/graphql/objects/#authoringpushedentry)!]! | List of all entries (page + nested blocks) touched by the push. |
| `locales` | [AuthoringPushLocales](/reference/graphql/objects/#authoringpushlocales)! | Which locale(s) the push wrote to. Localized fields land in |
| `pageEntryId` | `ID`! | Real Contentful entry id of the page itself. |
| `stats` | [AuthoringPushStats](/reference/graphql/objects/#authoringpushstats)! | Counts of created / updated / unchanged entries during the push. |
## AuthoringPushStats
[Section titled “AuthoringPushStats”](#authoringpushstats)
Counts returned by `authoringPushPage`.
| Field | Type | Description |
| ----------- | ------ | ----------- |
| `created` | `Int`! | |
| `unchanged` | `Int`! | |
| `updated` | `Int`! | |
## Breadcrumb
[Section titled “Breadcrumb”](#breadcrumb)
Represents a single item in a breadcrumb trail.
| Field | Type | Description |
| ------ | ----------------------------------------------------------- | ---------------------------- |
| `name` | `String`! | Name of the breadcrumb item. |
| `path` | `String`! | Path of the breadcrumb item. |
| `type` | [BreadcrumbType](/reference/graphql/enums/#breadcrumbtype)! | Type of the breadcrumb item. |
## BusinessUnit
[Section titled “BusinessUnit”](#businessunit)
| Field | Type | Description |
| ---------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `id` | `ID`! | Unique identifier for the business unit |
| `address` | [Address](/reference/graphql/objects/#address) | Address of the business unit *(args: `id`)* |
| `addresses` | [AddressConnection](/reference/graphql/objects/#addressconnection)! | Addresses of the business unit *(args: `page`, `pageSize`, `searchTerm`)* |
| `associate` | [BusinessUnitAssociate](/reference/graphql/objects/#businessunitassociate) | An associate of the business unit *(args: `id`)* |
| `associates` | [BusinessUnitAssociateConnection](/reference/graphql/objects/#businessunitassociateconnection)! | Associates of the business unit *(args: `page`, `pageSize`, `searchTerm`)* |
| `availableRoles` | \[[BusinessUnitRole](/reference/graphql/objects/#businessunitrole)!]! | Available roles that can be assigned to customers in this business unit |
| `billingAddress` | [Address](/reference/graphql/objects/#address) | Billing address of the business unit |
| `name` | `String`! | Name of the business unit |
| `shoppingList` | [ShoppingList](/reference/graphql/objects/#shoppinglist) | A shopping list of the business unit *(args: `id`)* |
| `shoppingLists` | [ShoppingListResult](/reference/graphql/objects/#shoppinglistresult) | Shopping lists of the business unit *(args: `page`, `pageSize`)* |
| `stores` | \[[Store](/reference/graphql/objects/#store)!] | Stores assigned to the business unit |
| `order` | [Order](/reference/graphql/objects/#order) | Get a specific order by order number *(args: `orderNumber`)* |
| `orders` | [OrderResult](/reference/graphql/objects/#orderresult) | Get a paginated list of orders for this business unit *(args: `page`, `pageSize`, `searchTerm`)* |
| `quote` | [Quote](/reference/graphql/objects/#quote) | Get a specific quote by its ID *(args: `id`)* |
| `quoteRequest` | [QuoteRequest](/reference/graphql/objects/#quoterequest) | Get a specific quote request by its ID *(args: `id`)* |
| `quoteRequests` | [QuoteRequestResult](/reference/graphql/objects/#quoterequestresult) | Get a paginated list of quote requests *(args: `page`, `pageSize`)* |
| `quotes` | [QuoteResult](/reference/graphql/objects/#quoteresult) | Get a paginated list of quotes *(args: `page`, `pageSize`)* |
## BusinessUnitAddressCreateResponse
[Section titled “BusinessUnitAddressCreateResponse”](#businessunitaddresscreateresponse)
| Field | Type | Description |
| -------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------- |
| `businessUnit` | [BusinessUnit](/reference/graphql/objects/#businessunit) | Updated business unit information with new address |
| `businessUnitErrors` | \[[BusinessUnitAddressCreateError](/reference/graphql/unions/#businessunitaddresscreateerror)!]! | Error details if address creation failed |
## BusinessUnitAddressDeleteResponse
[Section titled “BusinessUnitAddressDeleteResponse”](#businessunitaddressdeleteresponse)
| Field | Type | Description |
| -------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
| `businessUnit` | [BusinessUnit](/reference/graphql/objects/#businessunit) | Updated business unit information after address deletion |
| `businessUnitErrors` | \[[BusinessUnitAddressDeleteError](/reference/graphql/unions/#businessunitaddressdeleteerror)!]! | Error details if address deletion failed |
## BusinessUnitAddressUpdateResponse
[Section titled “BusinessUnitAddressUpdateResponse”](#businessunitaddressupdateresponse)
| Field | Type | Description |
| -------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
| `businessUnit` | [BusinessUnit](/reference/graphql/objects/#businessunit) | Updated business unit information with modified address |
| `businessUnitErrors` | \[[BusinessUnitAddressUpdateError](/reference/graphql/unions/#businessunitaddressupdateerror)!]! | Error details if address update failed |
## BusinessUnitAssociate
[Section titled “BusinessUnitAssociate”](#businessunitassociate)
Customer associated with a business unit, including their assigned roles
| Field | Type | Description |
| ------------ | --------------------------------------------------------------------- | --------------------------------------------------- |
| `createdAt` | [DateTime](/reference/graphql/scalars/#datetime)! | Date and time when the associate was created |
| `email` | [EmailAddress](/reference/graphql/scalars/#emailaddress)! | Email address of the associate |
| `familyName` | `String` | Last name of the associate |
| `givenName` | `String` | First name of the associate |
| `id` | `ID`! | Unique identifier for the associate |
| `roles` | \[[BusinessUnitRole](/reference/graphql/objects/#businessunitrole)!]! | Roles assigned to the customer in the business unit |
| `salutation` | [Salutation](/reference/graphql/enums/#salutation) | Optional salutation for the associate |
## BusinessUnitAssociateConnection
[Section titled “BusinessUnitAssociateConnection”](#businessunitassociateconnection)
Paginated connection for business unit associates
| Field | Type | Description |
| ------------ | ------------------------------------------------------------------------------- | --------------------------------------------- |
| `items` | \[[BusinessUnitAssociate](/reference/graphql/objects/#businessunitassociate)!]! | List of associate items in the current page |
| `totalCount` | `Int`! | Total number of associates matching the query |
## BusinessUnitAssociateInviteResponse
[Section titled “BusinessUnitAssociateInviteResponse”](#businessunitassociateinviteresponse)
| Field | Type | Description |
| -------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `businessUnit` | [BusinessUnit](/reference/graphql/objects/#businessunit) | Updated business unit information with new associate |
| `businessUnitErrors` | \[[BusinessUnitAssociateInviteError](/reference/graphql/unions/#businessunitassociateinviteerror)!]! | Error details if associate invitation failed |
## BusinessUnitAssociateRemoveResponse
[Section titled “BusinessUnitAssociateRemoveResponse”](#businessunitassociateremoveresponse)
| Field | Type | Description |
| -------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `businessUnit` | [BusinessUnit](/reference/graphql/objects/#businessunit) | Updated business unit information after associate removal |
| `businessUnitErrors` | \[[BusinessUnitAssociateRemoveError](/reference/graphql/unions/#businessunitassociateremoveerror)!]! | Error details if associate removal failed |
## BusinessUnitAssociateRolesUpdateResponse
[Section titled “BusinessUnitAssociateRolesUpdateResponse”](#businessunitassociaterolesupdateresponse)
| Field | Type | Description |
| -------------------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `businessUnit` | [BusinessUnit](/reference/graphql/objects/#businessunit) | Updated business unit information with modified associate roles |
| `businessUnitErrors` | \[[BusinessUnitAssociateRolesUpdateError](/reference/graphql/unions/#businessunitassociaterolesupdateerror)!]! | Error details if associate roles update failed |
## BusinessUnitRole
[Section titled “BusinessUnitRole”](#businessunitrole)
Roles that can be assigned to customers in a business unit
| Field | Type | Description |
| ------ | --------- | ------------------------------ |
| `id` | `ID`! | Unique identifier for the role |
| `name` | `String`! | Name of the role |
## Button
[Section titled “Button”](#button)
Represents a single button element.
| Field | Type | Description |
| --------- | --------------------------------------------------------- | --------------------------------- |
| `id` | `ID`! | Unique identifier for the button. |
| `link` | [ResourceLink](/reference/graphql/objects/#resourcelink)! | Link associated with the button. |
| `title` | `String`! | Title text of the button. |
| `variant` | [ButtonVariant](/reference/graphql/enums/#buttonvariant)! | Visual variant of the button. |
## Buttons
[Section titled “Buttons”](#buttons)
Represents a group of buttons.
| Field | Type | Description |
| --------- | ------------------------------------------------- | ---------------------------------------- |
| `buttons` | \[[Button](/reference/graphql/objects/#button)!]! | List of buttons in the group. |
| `id` | `ID`! | Unique identifier for the buttons group. |
## Cart
[Section titled “Cart”](#cart)
Shopping cart containing line items and checkout information
| Field | Type | Description |
| -------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `id` | `ID`! | Unique identifier of the cart |
| `availablePaymentMethods` | \[[PaymentMethod](/reference/graphql/objects/#paymentmethod)!]! | Payment methods that are available for the cart *(args: `device`)* |
| `availableShippingMethods` | \[[ShippingMethod](/reference/graphql/objects/#shippingmethod)!]! | Shipping methods that are available for the cart |
| `billingAddress` | [Address](/reference/graphql/objects/#address) | Customer billing address |
| `createdAt` | [DateTime](/reference/graphql/scalars/#datetime)! | Creation date of the cart |
| `currency` | `String`! | The currency used in the cart |
| `customerEmail` | `String` | Customer email |
| `discountCodes` | \[[CartDiscountCode](/reference/graphql/objects/#cartdiscountcode)!]! | Discount codes that are applied to the cart |
| `discountTotal` | [Money](/reference/graphql/objects/#money) | Sum of all discounts |
| `isGuest` | `Boolean`! | Indicates if the cart is a guest cart |
| `lastModifiedAt` | [DateTime](/reference/graphql/scalars/#datetime)! | Last modified date of the cart |
| `lineItems` | \[[CartLineItem](/reference/graphql/interfaces/#cartlineitem)!]! | Line items in the cart |
| `selectedPaymentMethod` | [PaymentMethod](/reference/graphql/objects/#paymentmethod) | Selected payment method for the cart |
| `selectedShippingMethod` | [ShippingMethod](/reference/graphql/objects/#shippingmethod) | Selected shipping method for the cart |
| `shippingAddress` | [Address](/reference/graphql/objects/#address) | Customer shipping address |
| `shippingCosts` | [TaxedPrice](/reference/graphql/objects/#taxedprice) | Shipping costs, if any |
| `subTotal` | [TaxedTotalPrice](/reference/graphql/objects/#taxedtotalprice)! | Sum of all line items (i.e. price excluding all shipping and payment costs) |
| `total` | [TaxedTotalPrice](/reference/graphql/objects/#taxedtotalprice)! | Sum of all line items (i.e. price including all shipping and payment costs) |
| `totalQuantity` | `Int`! | Total quantity of all line items |
## CartDiscountCode
[Section titled “CartDiscountCode”](#cartdiscountcode)
A CartDiscountCode represents a discount that is applied to the cart
| Field | Type | Description |
| -------- | ------------------------------------------- | --------------------------------------- |
| `amount` | [Money](/reference/graphql/objects/#money)! | The discount amount applied |
| `code` | `String`! | The discount code string used |
| `id` | `ID`! | Unique identifier for the discount code |
## CartDiscountCodeAddResponse
[Section titled “CartDiscountCodeAddResponse”](#cartdiscountcodeaddresponse)
Response for adding discount code to cart
| Field | Type | Description |
| ------------ | ------------------------------------------------------------------------------------ | ---------------------------------------------- |
| `cart` | [Cart](/reference/graphql/objects/#cart) | Cart information after discount code is added |
| `cartErrors` | \[[CartDiscountCodeAddError](/reference/graphql/unions/#cartdiscountcodeadderror)!]! | Error details if discount code addition failed |
## CartDiscountCodeRemoveResponse
[Section titled “CartDiscountCodeRemoveResponse”](#cartdiscountcoderemoveresponse)
Response for removing discount code from cart
| Field | Type | Description |
| ------------ | ------------------------------------------------------------------------------------------ | ----------------------------------------------- |
| `cart` | [Cart](/reference/graphql/objects/#cart) | Cart information after discount code is removed |
| `cartErrors` | \[[CartDiscountCodeRemoveError](/reference/graphql/unions/#cartdiscountcoderemoveerror)!]! | Error details if discount code removal failed |
## CartError
[Section titled “CartError”](#carterror)
A recoverable error that occurred during a cart operation
**Implements:** [Error](/reference/graphql/interfaces/#error)
| Field | Type | Description |
| --------- | --------------------------------------------------------- | ----------------------------------------------------- |
| `code` | [CartErrorCode](/reference/graphql/enums/#carterrorcode)! | The error code identifying the cart operation failure |
| `message` | `String`! | |
## CartLineItemsAddResponse
[Section titled “CartLineItemsAddResponse”](#cartlineitemsaddresponse)
Response for adding line items to cart
| Field | Type | Description |
| ------------ | ------------------------------------------------------------------------------ | ------------------------------------------- |
| `cart` | [Cart](/reference/graphql/objects/#cart) | Cart information after line items are added |
| `cartErrors` | \[[CartLineItemsAddError](/reference/graphql/unions/#cartlineitemsadderror)!]! | Error details if line item addition failed |
## CartLineItemsRemoveResponse
[Section titled “CartLineItemsRemoveResponse”](#cartlineitemsremoveresponse)
Response for removing line items from cart
| Field | Type | Description |
| ------------ | ------------------------------------------------------------------------------------ | --------------------------------------------- |
| `cart` | [Cart](/reference/graphql/objects/#cart) | Cart information after line items are removed |
| `cartErrors` | \[[CartLineItemsRemoveError](/reference/graphql/unions/#cartlineitemsremoveerror)!]! | Error details if line item removal failed |
## CartLineItemsUpdateResponse
[Section titled “CartLineItemsUpdateResponse”](#cartlineitemsupdateresponse)
Response for updating line items in cart
| Field | Type | Description |
| ------------ | ------------------------------------------------------------------------------------ | --------------------------------------------- |
| `cart` | [Cart](/reference/graphql/objects/#cart) | Cart information after line items are updated |
| `cartErrors` | \[[CartLineItemsUpdateError](/reference/graphql/unions/#cartlineitemsupdateerror)!]! | Error details if line item update failed |
## CartSingleLineItem
[Section titled “CartSingleLineItem”](#cartsinglelineitem)
Individual line item in a cart
**Implements:** [CartLineItem](/reference/graphql/interfaces/#cartlineitem)
| Field | Type | Description |
| ---------------- | ------------------------------------------------------------- | ----------------------------------------- |
| `addedAt` | `String` | Date when the line item was added |
| `availableStock` | `Int`! | Available stock for the product variant |
| `id` | `ID`! | Unique identifier for the line item |
| `listPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The list price (MSRP) |
| `price` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The final price after applying discounts |
| `productName` | `String`! | Product name for the line item |
| `productSlug` | `String` | Product slug for URL generation |
| `quantity` | `Int`! | Number of items in the line item |
| `regularPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The regular selling price on the platform |
| `variant` | [ProductVariant](/reference/graphql/objects/#productvariant)! | Product variant details |
## CartUpdateResponse
[Section titled “CartUpdateResponse”](#cartupdateresponse)
Response for cart update operations
| Field | Type | Description |
| ------------ | ------------------------------------------------------------------ | ------------------------------ |
| `cart` | [Cart](/reference/graphql/objects/#cart) | Cart information after update |
| `cartErrors` | \[[CartUpdateError](/reference/graphql/unions/#cartupdateerror)!]! | Error details if update failed |
## CatalogBlock
[Section titled “CatalogBlock”](#catalogblock)
Represents a block of catalog pages, typically used for navigation or categorization.
| Field | Type | Description |
| -------- | --------------------------------------------------------------------- | --------------------------------------------------- |
| `format` | [CatalogBlockFormat](/reference/graphql/enums/#catalogblockformat)! | Format of the catalog block, e.g., ‘round’, ‘card’. |
| `id` | `ID`! | Unique identifier for the catalog block. |
| `items` | \[[CatalogBlockItem](/reference/graphql/objects/#catalogblockitem)!]! | List of catalog block items. |
| `title` | `String`! | Title of the catalog block. |
## CatalogBlockItem
[Section titled “CatalogBlockItem”](#catalogblockitem)
Represents a catalog block item with its associated content.
| Field | Type | Description |
| ------- | --------------------------------------------------------- | --------------------------------------------- |
| `id` | `ID`! | Unique identifier for the catalog block item. |
| `image` | [ContentAsset](/reference/graphql/objects/#contentasset)! | Image asset for the catalog block item. |
| `link` | [ResourceLink](/reference/graphql/objects/#resourcelink)! | Link associated with the catalog block item. |
| `title` | `String`! | Title of the catalog block item. |
## CatalogPage
[Section titled “CatalogPage”](#catalogpage)
Represents a catalog page with catalog specific content, e.g. a product listing of all products in a category or a pre-defined search query.
**Implements:** [AbstractPage](/reference/graphql/interfaces/#abstractpage)
| Field | Type | Description |
| ---------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `id` | `ID`! | Unique identifier for the product listing page. |
| `categoryId` | `String` | Identifier of the category associated with this product listing. |
| `productListingConfig` | [ProductListingConfig](/reference/graphql/objects/#productlistingconfig)! | |
| `bottomContent` | \[[Block](/reference/graphql/unions/#block)!] | Bottom content of the product listing page, composed of various block types. |
| `breadcrumbs` | \[[Breadcrumb](/reference/graphql/objects/#breadcrumb)!] | Breadcrumb trail for the product listing page. |
| `hero` | [HeroBlock](/reference/graphql/objects/#heroblock) | Hero section of the product listing page. |
| `meta` | [PageMeta](/reference/graphql/objects/#pagemeta) | Page metadata for SEO and other purposes. |
| `name` | `String`! | Name of the product listing page. |
| `path` | `String`! | URL path of the product listing page. |
| `teaserRow` | [TeasersBlock](/reference/graphql/objects/#teasersblock) | Teaser row displayed on the product listing page. |
| `topContent` | \[[Block](/reference/graphql/unions/#block)!] | Top content of the product listing page, composed of various block types. |
| `translatedPaths` | \[[TranslatedPath](/reference/graphql/objects/#translatedpath)!] | List of translated paths for different locales. |
## CategoryFacet
[Section titled “CategoryFacet”](#categoryfacet)
Category-based facet for hierarchical filtering (e.g., categories, collections)
| Field | Type | Description |
| --------- | --------------------------------------------------------- | ------------------------------------------------- |
| `key` | `String`! | Unique key for the facet (e.g., ‘sustainable’) |
| `label` | `String`! | Display label for the facet (e.g., ‘Sustainable’) |
| `options` | \[[FacetValue](/reference/graphql/objects/#facetvalue)!]! | Available options for this facet |
## CategoryOverviewPage
[Section titled “CategoryOverviewPage”](#categoryoverviewpage)
Category overview page displaying child categories and navigation
| Field | Type | Description |
| ----------------- | ------------------------------------------------------------------- | ------------------------------------------------------------- |
| `breadcrumbs` | \[[Breadcrumb](/reference/graphql/objects/#breadcrumb)!]! | The breadcrumbs representing the navigation path to the page. |
| `category` | [ProductCategory](/reference/graphql/objects/#productcategory) | The parent category for this overview page |
| `childCategories` | \[[ProductCategory](/reference/graphql/objects/#productcategory)!]! | Child categories displayed on this overview page |
| `indexable` | `Boolean` | Indicates whether the page is indexable for search engines. |
| `meta` | [PageMeta](/reference/graphql/objects/#pagemeta) | Page metadata for SEO and other purposes |
| `name` | `String` | The name of the page. |
| `path` | `String`! | The URL path of the category overview page |
## CheckoutCompleted
[Section titled “CheckoutCompleted”](#checkoutcompleted)
Order information returned after successful checkout completion
| Field | Type | Description |
| ------------- | --------- | -------------------------------------- |
| `orderId` | `String`! | Unique identifier of the created order |
| `orderNumber` | `String`! | Human-readable order number |
## CheckoutCompleteError
[Section titled “CheckoutCompleteError”](#checkoutcompleteerror)
Error that occurred during checkout completion
**Implements:** [Error](/reference/graphql/interfaces/#error)
| Field | Type | Description |
| --------- | --------------------------------------------------------------------------------- | ---------------------------------------- |
| `code` | [CheckoutCompleteErrorCode](/reference/graphql/enums/#checkoutcompleteerrorcode)! | Error code identifying the type of error |
| `message` | `String`! | Human-readable error message |
## CheckoutCompleteResponse
[Section titled “CheckoutCompleteResponse”](#checkoutcompleteresponse)
Response for checkout completion
| Field | Type | Description |
| ------------------------ | ---------------------------------------------------------------------------------------- | ------------------------------------------- |
| `checkoutCompleteErrors` | \[[CheckoutCompleteErrorUnion](/reference/graphql/unions/#checkoutcompleteerrorunion)!]! | Error details if checkout completion failed |
| `redirectURL` | `String` | URL to redirect the customer to for payment |
## ContentAsset
[Section titled “ContentAsset”](#contentasset)
Represents a content asset, such as an image or file.
| Field | Type | Description |
| --------------- | --------- | --------------------------------------- |
| `alt` | `String` | Alternative text for the asset. |
| `filename` | `String`! | Filename of the asset. |
| `isExternalUrl` | `Boolean` | Indicates if the asset URL is external. |
| `name` | `String` | Name of the asset. |
| `title` | `String` | Title of the asset. |
## ContentPage
[Section titled “ContentPage”](#contentpage)
Represents a content page with various sections and blocks.
**Implements:** [AbstractPage](/reference/graphql/interfaces/#abstractpage)
| Field | Type | Description |
| ----------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- |
| `id` | `ID`! | Unique identifier for the content page. |
| `body` | \[[Block](/reference/graphql/unions/#block)] | Main content body of the page, composed of various block types. |
| `breadcrumbs` | \[[Breadcrumb](/reference/graphql/objects/#breadcrumb)!] | Breadcrumb trail for the content page. |
| `hero` | [HeroBlock](/reference/graphql/objects/#heroblock) | Hero section of the content page. |
| `meta` | [PageMeta](/reference/graphql/objects/#pagemeta) | Page metadata for SEO and other purposes. |
| `name` | `String`! | Name of the content page. |
| `path` | `String`! | URL path of the content page. |
| `translatedPaths` | \[[TranslatedPath](/reference/graphql/objects/#translatedpath)!] | List of translated paths for different locales. |
## ContentSnippet
[Section titled “ContentSnippet”](#contentsnippet)
Represents a snippet of content that can be used in various places.
| Field | Type | Description |
| --------- | --------------------------------------------- | ------------------------------------------ |
| `content` | \[[Block](/reference/graphql/unions/#block)!] | Content for the snippet. |
| `id` | `ID`! | Unique identifier for the content snippet. |
| `key` | `String`! | Key for identifying the content snippet. |
## ContentTable
[Section titled “ContentTable”](#contenttable)
Represents a table structure in content.
| Field | Type | Description |
| ------- | ---------------- | ----------------- |
| `tbody` | \[\[`String`!]!] | Table body rows. |
| `thead` | \[`String`!] | Table header row. |
## CreateGuestSessionResponse
[Section titled “CreateGuestSessionResponse”](#createguestsessionresponse)
Response type for creating a guest session
| Field | Type | Description |
| --------- | ---------- | -------------------------------------------------- |
| `success` | `Boolean`! | Whether the guest session was created successfully |
## CreatePaymentResponse
[Section titled “CreatePaymentResponse”](#createpaymentresponse)
Response for creating a payment
| Field | Type | Description |
| --------------- | ---------------------------------------------------------------------------------- | ------------------------------------------ |
| `payment` | [PaymentData](/reference/graphql/objects/#paymentdata) | Payment data if payment creation succeeded |
| `paymentErrors` | \[[CreatePaymentErrorUnion](/reference/graphql/unions/#createpaymenterrorunion)!]! | Error details if payment creation failed |
## Customer
[Section titled “Customer”](#customer)
Customer account information with personal details and business context
| Field | Type | Description |
| ---------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `email` | `String`! | Email address of the customer |
| `addresses` | \[[Address](/reference/graphql/objects/#address)!] | List of addresses associated with the customer |
| `billingAddress` | [Address](/reference/graphql/objects/#address) | Primary billing address for the customer |
| `businessUnit` | [BusinessUnit](/reference/graphql/objects/#businessunit) | Get a specific business unit by ID that the customer has access to *(args: `id`)* |
| `businessUnits` | \[[BusinessUnit](/reference/graphql/objects/#businessunit)!] | List of all business units the customer has access to |
| `company` | `String` | Company name for business customers |
| `createdAt` | [DateTime](/reference/graphql/scalars/#datetime)! | Date and time when the customer account was created |
| `familyName` | `String` | Last name of the customer |
| `givenName` | `String` | First name of the customer |
| `id` | `ID`! | Unique identifier for the customer |
| `salutation` | [Salutation](/reference/graphql/enums/#salutation) | Optional salutation for the customer |
| `shoppingList` | [ShoppingList](/reference/graphql/objects/#shoppinglist) | Get a specific shopping list by ID for the customer *(args: `id`)* |
| `shoppingLists` | [ShoppingListResult](/reference/graphql/objects/#shoppinglistresult) | Get paginated shopping lists for the customer *(args: `page`, `pageSize`)* |
| `vatNumber` | `String` | VAT identification number for business customers |
| `order` | [Order](/reference/graphql/objects/#order) | Get a specific order by order number *(args: `orderNumber`)* |
| `orders` | [OrderResult](/reference/graphql/objects/#orderresult) | Get a paginated list of orders for this customer *(args: `page`, `pageSize`, `searchTerm`)* |
## CustomerAddressCreateResponse
[Section titled “CustomerAddressCreateResponse”](#customeraddresscreateresponse)
| Field | Type | Description |
| ---------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `customer` | [Customer](/reference/graphql/objects/#customer) | Updated customer account information with new address |
| `customerErrors` | \[[CustomerAddressCreateError](/reference/graphql/unions/#customeraddresscreateerror)!]! | Error details if address creation failed |
## CustomerAddressDeleteResponse
[Section titled “CustomerAddressDeleteResponse”](#customeraddressdeleteresponse)
| Field | Type | Description |
| ---------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `customer` | [Customer](/reference/graphql/objects/#customer) | Updated customer account information after address deletion |
| `customerErrors` | \[[CustomerAddressDeleteError](/reference/graphql/unions/#customeraddressdeleteerror)!]! | Error details if address deletion failed |
## CustomerAddressUpdateResponse
[Section titled “CustomerAddressUpdateResponse”](#customeraddressupdateresponse)
| Field | Type | Description |
| ---------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `customer` | [Customer](/reference/graphql/objects/#customer) | Updated customer account information with modified address |
| `customerErrors` | \[[CustomerAddressUpdateError](/reference/graphql/unions/#customeraddressupdateerror)!]! | Error details if address update failed |
## CustomerCreateResponse
[Section titled “CustomerCreateResponse”](#customercreateresponse)
| Field | Type | Description |
| ---------------- | -------------------------------------------------------------------------- | ---------------------------------------- |
| `customer` | [Customer](/reference/graphql/objects/#customer) | Created customer account information |
| `customerErrors` | \[[CustomerCreateError](/reference/graphql/unions/#customercreateerror)!]! | Error details if account creation failed |
## CustomerDeleteResponse
[Section titled “CustomerDeleteResponse”](#customerdeleteresponse)
| Field | Type | Description |
| ---------------- | -------------------------------------------------------------------------- | ---------------------------------------- |
| `customerErrors` | \[[CustomerDeleteError](/reference/graphql/unions/#customerdeleteerror)!]! | Error details if deletion failed |
| `success` | `Boolean` | Indicates if the deletion was successful |
## CustomerEmailUpdateResponse
[Section titled “CustomerEmailUpdateResponse”](#customeremailupdateresponse)
| Field | Type | Description |
| ---------------- | ------------------------------------------------------------------------------------ | ------------------------------------ |
| `customer` | [Customer](/reference/graphql/objects/#customer) | Updated customer account information |
| `customerErrors` | \[[CustomerEmailUpdateError](/reference/graphql/unions/#customeremailupdateerror)!]! | Error details if email update failed |
## CustomerExistsResult
[Section titled “CustomerExistsResult”](#customerexistsresult)
Result type for checking if a customer exists
| Field | Type | Description |
| -------- | ---------- | ---------------------------------------------- |
| `result` | `Boolean`! | Whether a customer with the given email exists |
## CustomerInformationUpdateResponse
[Section titled “CustomerInformationUpdateResponse”](#customerinformationupdateresponse)
| Field | Type | Description |
| ---------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------ |
| `customer` | [Customer](/reference/graphql/objects/#customer) | Updated customer account information |
| `customerErrors` | \[[CustomerInformationUpdateError](/reference/graphql/unions/#customerinformationupdateerror)!]! | Error details if update failed |
## CustomerLoginResponse
[Section titled “CustomerLoginResponse”](#customerloginresponse)
| Field | Type | Description |
| ---------------- | ------------------------------------------------------------------------ | ------------------------------------------ |
| `customer` | [Customer](/reference/graphql/objects/#customer) | Authenticated customer account information |
| `customerErrors` | \[[CustomerLoginError](/reference/graphql/unions/#customerloginerror)!]! | Error details if authentication failed |
## CustomerLogoutResponse
[Section titled “CustomerLogoutResponse”](#customerlogoutresponse)
| Field | Type | Description |
| ---------------- | -------------------------------------------------- | ------------------------------------------ |
| `customerErrors` | \[[Error](/reference/graphql/interfaces/#error)!]! | Error details if authentication failed |
| `success` | `Boolean`! | Authenticated customer account information |
## CustomerPasswordResetRequestResponse
[Section titled “CustomerPasswordResetRequestResponse”](#customerpasswordresetrequestresponse)
| Field | Type | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ |
| `customerErrors` | \[[CustomerPasswordResetRequestError](/reference/graphql/unions/#customerpasswordresetrequesterror)!]! | Error details if the request failed |
| `success` | `Boolean`! | Indicates if the password reset request was successful |
## CustomerPasswordResetResponse
[Section titled “CustomerPasswordResetResponse”](#customerpasswordresetresponse)
| Field | Type | Description |
| ---------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `customer` | [Customer](/reference/graphql/objects/#customer) | Updated customer account information after password reset |
| `customerErrors` | \[[CustomerPasswordResetError](/reference/graphql/unions/#customerpasswordreseterror)!]! | Error details if the password reset failed |
## CustomerPasswordUpdateResponse
[Section titled “CustomerPasswordUpdateResponse”](#customerpasswordupdateresponse)
| Field | Type | Description |
| ---------------- | ------------------------------------------------------------------------------------------ | --------------------------------------------------------- |
| `customer` | [Customer](/reference/graphql/objects/#customer) | Updated customer account information after password reset |
| `customerErrors` | \[[CustomerPasswordUpdateError](/reference/graphql/unions/#customerpasswordupdateerror)!]! | Error details if the password update failed |
## Dimension
[Section titled “Dimension”](#dimension)
Dimension option specific for this variant (see ProductVariantDimension for available options)
| Field | Type | Description |
| ------------ | --------- | ------------------------------------------------------------------ |
| `key` | `String`! | The key of the dimension (e.g. ‘color’) |
| `label` | `String`! | The translated label of the dimension (e.g. ‘Color’ or ‘Kleur’) |
| `value` | `String`! | The value of the dimension (e.g. ‘#ff0000’) |
| `valueLabel` | `String`! | The translated value label of the dimension (e.g. ‘Red’ or ‘Rood’) |
## Divider
[Section titled “Divider”](#divider)
Represents a divider element, typically used for visual separation in content.
| Field | Type | Description |
| ----- | ----- | ---------------------------------- |
| `id` | `ID`! | Unique identifier for the divider. |
## FacetValue
[Section titled “FacetValue”](#facetvalue)
Individual value option within a facet with count information
| Field | Type | Description |
| ---------- | ---------- | ----------------------------------------------- |
| `count` | `Int`! | Number of products matching this facet value |
| `key` | `String`! | Unique key for the facet value (e.g., ‘red’) |
| `label` | `String`! | Display label for the facet value (e.g., ‘Red’) |
| `selected` | `Boolean`! | Whether this facet value is currently selected |
## Faq
[Section titled “Faq”](#faq)
| Field | Type | Description |
| -------- | -------------------------------------------------------- | ------------------------------ |
| `groups` | \[[FaqGroup](/reference/graphql/objects/#faqgroup)!]! | |
| `id` | `ID`! | Unique identifier for the Faq. |
| `link` | [ResourceLink](/reference/graphql/objects/#resourcelink) | |
| `title` | `String`! | |
## FaqGroup
[Section titled “FaqGroup”](#faqgroup)
| Field | Type | Description |
| ------- | --------------------------------------------------- | ------------------------------------ |
| `id` | `ID`! | Unique identifier for the Faq group. |
| `items` | \[[FaqItem](/reference/graphql/objects/#faqitem)!]! | List of FAQ items. |
| `title` | `String`! | Title of the FAQ item list. |
## FaqItem
[Section titled “FaqItem”](#faqitem)
| Field | Type | Description |
| ---------- | ------------------------------------------------- | ----------------------------------- |
| `answer` | [RichText](/reference/graphql/objects/#richtext)! | |
| `id` | `ID`! | Unique identifier for the Faq item. |
| `question` | `String`! | |
## FieldValidationIssue
[Section titled “FieldValidationIssue”](#fieldvalidationissue)
| Field | Type | Description |
| --------- | --------- | ------------------------------------------------------------------------- |
| `code` | `String`! | The error code for the validation issue |
| `field` | `String`! | |
| `message` | `String`! | human-readable message describing the error. This message is not intended |
## FilterCondition
[Section titled “FilterCondition”](#filtercondition)
| Field | Type | Description |
| ------- | --------- | ---------------------- |
| `key` | `String`! | Filter condition key |
| `value` | `String`! | Filter condition value |
## HeroBlock
[Section titled “HeroBlock”](#heroblock)
Represents a hero section typically used at the top of a page.
| Field | Type | Description |
| --------------- | --------------------------------------------------------- | ------------------------------------------ |
| `bodyText` | [RichText](/reference/graphql/objects/#richtext) | Body text of the hero section. |
| `buttons` | \[[Button](/reference/graphql/objects/#button)!] | Buttons displayed in the hero section. |
| `id` | `ID`! | Unique identifier for the hero section. |
| `image` | [ContentAsset](/reference/graphql/objects/#contentasset)! | Image asset used in the hero section. |
| `imagePosition` | [ImagePosition](/reference/graphql/enums/#imageposition)! | Position of the image in the hero section. |
| `imageStyle` | [ImageStyle](/reference/graphql/enums/#imagestyle)! | Style of the image in the hero section. |
| `title` | `String`! | Title of the hero section. |
## Image
[Section titled “Image”](#image)
Represents a single image with its associated content.
| Field | Type | Description |
| ------- | --------------------------------------------------------- | -------------------------------- |
| `id` | `ID`! | Unique identifier for the image. |
| `image` | [ContentAsset](/reference/graphql/objects/#contentasset)! | |
## InfoPages
[Section titled “InfoPages”](#infopages)
Represents links to various information pages of a website.
| Field | Type | Description |
| ---------- | -------------------------------------------------------- | -------------------------------------- |
| `business` | [ResourceLink](/reference/graphql/objects/#resourcelink) | Link to the business information page. |
| `cookies` | [ResourceLink](/reference/graphql/objects/#resourcelink) | Link to the cookies policy page. |
| `privacy` | [ResourceLink](/reference/graphql/objects/#resourcelink) | Link to the privacy policy page. |
| `service` | [ResourceLink](/reference/graphql/objects/#resourcelink) | Link to the customer service page. |
| `terms` | [ResourceLink](/reference/graphql/objects/#resourcelink) | Link to the terms and conditions page. |
## LocalizedSlug
[Section titled “LocalizedSlug”](#localizedslug)
Localized URL slug for different language versions
| Field | Type | Description |
| -------- | --------- | ------------------------ |
| `locale` | `String`! | The locale for this slug |
| `slug` | `String`! | The localized slug value |
## MenuBlock
[Section titled “MenuBlock”](#menublock)
Represents a block of menu items.
| Field | Type | Description |
| ----------- | ------------------------------------------------------------- | ------------------------------------- |
| `id` | `ID`! | Unique identifier for the menu block. |
| `label` | `String`! | Label for the menu block. |
| `menuLinks` | \[[ResourceLink](/reference/graphql/objects/#resourcelink)!]! | List of links in the menu block. |
## MenuItem
[Section titled “MenuItem”](#menuitem)
Represents a single item in a menu.
| Field | Type | Description |
| -------------- | ----------------------------------------------------------- | ------------------------------------------ |
| `id` | `ID`! | Unique identifier for the menu item. |
| `label` | `String` | Label text for the menu item. |
| `subMenuItems` | \[[MenuItem](/reference/graphql/objects/#menuitem)!] | Sub-menu items under this menu item. |
| `target` | [MenuLinkTarget](/reference/graphql/unions/#menulinktarget) | Target link or category for the menu item. |
## Money
[Section titled “Money”](#money)
| Field | Type | Description |
| ------------ | ------------------------------------------------- | ----------- |
| `centAmount` | `Int`! | |
| `currency` | [Currency](/reference/graphql/scalars/#currency)! | |
## NavigationMenu
[Section titled “NavigationMenu”](#navigationmenu)
| Field | Type | Description |
| ----------- | ----------------------------------------------------------------------- | ---------------------------------------------------- |
| `id` | `ID`! | Unique identifier for the navigation menu. |
| `key` | `String`! | Key for identifying the navigation menu for a brand. |
| `menuItems` | \[[NavigationMenuItem](/reference/graphql/objects/#navigationmenuitem)] | Top level navigation menu items. |
## NavigationMenuItem
[Section titled “NavigationMenuItem”](#navigationmenuitem)
| Field | Type | Description |
| -------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------- |
| `extraContent` | \[[NavigationContent](/reference/graphql/unions/#navigationcontent)] | Extra content shown in the navigation menu item. |
| `icon` | [ContentAsset](/reference/graphql/objects/#contentasset) | Icon for the navigation menu item. |
| `id` | `ID`! | Unique identifier for the navigation menu item. |
| `link` | [ResourceLink](/reference/graphql/objects/#resourcelink) | Link to the resource associated with the navigation menu item. |
| `menuItems` | \[[NavigationMenuItem](/reference/graphql/objects/#navigationmenuitem)] | Sub menu items within the navigation menu item. |
| `name` | `String` | Label text for the navigation menu item. |
## OneColumnBlock
[Section titled “OneColumnBlock”](#onecolumnblock)
Represents a one-column layout structure.
| Field | Type | Description |
| --------- | ---------------------------------------------------------- | -------------------------------------------- |
| `column1` | \[[ColumnEntry](/reference/graphql/unions/#columnentry)!]! | Content blocks for the first column. |
| `id` | `ID`! | Unique identifier for the one-column layout. |
## OpenGraph
[Section titled “OpenGraph”](#opengraph)
Open Graph protocol metadata for social media sharing
| Field | Type | Description |
| ------------- | -------- | ----------------------- |
| `description` | `String` | Open Graph description. |
| `image` | `String` | Open Graph image URL. |
| `title` | `String` | Open Graph title. |
## OptionsFacet
[Section titled “OptionsFacet”](#optionsfacet)
Multi-option facet for categorical filtering (e.g., color, brand)
| Field | Type | Description |
| --------- | --------------------------------------------------------- | ------------------------------------------- |
| `key` | `String`! | Unique key for the facet (e.g., ‘color’) |
| `label` | `String`! | Display label for the facet (e.g., ‘Color’) |
| `options` | \[[FacetValue](/reference/graphql/objects/#facetvalue)!]! | Available options for this facet |
## Order
[Section titled “Order”](#order)
Complete order information including customer details, line items, and status
| Field | Type | Description |
| ------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `id` | `ID`! | Unique identifier for the order |
| `billingAddress` | [Address](/reference/graphql/objects/#address) | Billing address for the order |
| `availablePaymentMethods` | \[[PaymentMethod](/reference/graphql/objects/#paymentmethod)!]! | Available payment methods for the order. Country is required to determine the available payment methods *(args: `device`)* |
| `createdAt` | [DateTime](/reference/graphql/scalars/#datetime)! | Date and time when the order was created |
| `createdBy` | `String` | The user or system that created the order |
| `currency` | `String`! | Currency used for the order |
| `customerEmail` | [EmailAddress](/reference/graphql/scalars/#emailaddress) | Email address of the customer who placed the order |
| `discountCodes` | \[[OrderDiscountCode](/reference/graphql/objects/#orderdiscountcode)!]! | Discount codes that were applied to the cart |
| `discountTotal` | [Money](/reference/graphql/objects/#money) | Sum of all discounts |
| `isGuest` | `Boolean`! | Indicates if the order was placed by a guest customer |
| `lineItems` | \[[OrderLineItem](/reference/graphql/interfaces/#orderlineitem)!]! | Line items in the order |
| `orderNumber` | `String`! | Human-readable order number |
| `orderStatus` | [OrderStatus](/reference/graphql/enums/#orderstatus) | OrderStatus contains a general status of the order. This is useful for |
| `selectedPaymentMethod` | [PaymentMethod](/reference/graphql/objects/#paymentmethod) | Payment method selected for the order |
| `shippingAddress` | [Address](/reference/graphql/objects/#address) | Shipping address for the order |
| `shippingCosts` | [TaxedPrice](/reference/graphql/objects/#taxedprice) | Shipping costs for the order |
| `shippingNotice` | `String` | Special shipping notice or instructions |
| `subTotal` | [TaxedTotalPrice](/reference/graphql/objects/#taxedtotalprice)! | Sum of all line items (i.e. price excluding all shipping and payment costs) |
| `total` | [TaxedTotalPrice](/reference/graphql/objects/#taxedtotalprice)! | Total price of the order including all costs |
| `vatShifting` | [Money](/reference/graphql/objects/#money) | VAT shifting amount for reverse charge mechanism. This applies when the VAT |
## OrderDiscountCode
[Section titled “OrderDiscountCode”](#orderdiscountcode)
An OrderDiscountCode represents a discount that was applied to the order. These are identified by their discount code.
| Field | Type | Description |
| -------- | ------------------------------------------- | --------------------------------------- |
| `amount` | [Money](/reference/graphql/objects/#money)! | The discount amount applied |
| `code` | `String`! | The discount code string used |
| `id` | `ID`! | Unique identifier for the discount code |
## OrderResult
[Section titled “OrderResult”](#orderresult)
Paginated result containing orders and metadata
| Field | Type | Description |
| --------- | ----------------------------------------------- | ------------------------------------------------- |
| `results` | \[[Order](/reference/graphql/objects/#order)!]! | List of orders returned in the result |
| `total` | `Int`! | Total count of orders matching the query criteria |
## OrderSingleLineItem
[Section titled “OrderSingleLineItem”](#ordersinglelineitem)
Individual line item in an order containing product details and pricing information
**Implements:** [OrderLineItem](/reference/graphql/interfaces/#orderlineitem)
| Field | Type | Description |
| -------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `id` | `ID`! | Unique identifier for the line item |
| `listPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | Represents the list price, also known as te Manufacturer’s Suggested Retail |
| `name` | `String`! | Name of the line item |
| `price` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The line-item is the final price after applying any additional discounts on |
| `quantity` | `Int`! | Number of items in the line item |
| `regularPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The regular selling price on the platform, potentially already discounted |
| `variant` | [ProductVariant](/reference/graphql/objects/#productvariant)! | Product variant details for this line item |
## OtherPaymentMethodExtra
[Section titled “OtherPaymentMethodExtra”](#otherpaymentmethodextra)
**Implements:** [PaymentMethodExtraFields](/reference/graphql/interfaces/#paymentmethodextrafields)
| Field | Type | Description |
| ------ | --------- | ----------- |
| `type` | `String`! | |
## PageMeta
[Section titled “PageMeta”](#pagemeta)
Page metadata for SEO
| Field | Type | Description |
| ------------- | -------------------------------------------------- | -------------------------------- |
| `description` | `String` | Description of the page |
| `keywords` | \[`String`] | SEO keywords for the page |
| `openGraph` | [OpenGraph](/reference/graphql/objects/#opengraph) | Open Graph metadata for the page |
| `title` | `String` | Title of the page |
## PagesResult
[Section titled “PagesResult”](#pagesresult)
Represents the result of a query for multiple pages.
| Field | Type | Description |
| --------- | -------------------------------------------- | ----------------------------------------- |
| `results` | \[[Page](/reference/graphql/unions/#page)!]! | List of pages returned in the result. |
| `total` | `Int`! | Total number of pages matching the query. |
## PageTitle
[Section titled “PageTitle”](#pagetitle)
Represents a page title.
| Field | Type | Description |
| ------- | --------- | ------------------------------------- |
| `id` | `ID`! | Unique identifier for the page title. |
| `title` | `String`! | |
## PasswordChangeError
[Section titled “PasswordChangeError”](#passwordchangeerror)
**Implements:** [Error](/reference/graphql/interfaces/#error)
| Field | Type | Description |
| --------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `code` | [PasswordChangeErrorCode](/reference/graphql/enums/#passwordchangeerrorcode)! | Error code indicating the specific password change failure |
| `message` | `String`! | Human readable error message describing the password change failure. Not intended for end users. |
## PasswordResetError
[Section titled “PasswordResetError”](#passwordreseterror)
**Implements:** [Error](/reference/graphql/interfaces/#error)
| Field | Type | Description |
| --------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `code` | [PasswordResetErrorCode](/reference/graphql/enums/#passwordreseterrorcode)! | Error code indicating the specific password reset failure |
| `message` | `String`! | Human readable error message describing the password reset failure. Not intended for end users. |
## Payment
[Section titled “Payment”](#payment)
| Field | Type | Description |
| ----- | ----- | ----------- |
| `id` | `ID`! | |
## PaymentCreateError
[Section titled “PaymentCreateError”](#paymentcreateerror)
Error that occurred during payment creation
**Implements:** [Error](/reference/graphql/interfaces/#error)
| Field | Type | Description |
| --------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `code` | [PaymentCreateErrorCode](/reference/graphql/enums/#paymentcreateerrorcode)! | Error code indicating the specific payment creation failure |
| `message` | `String`! | Human readable error message describing the payment creation failure. Not intended for end users. |
## PaymentData
[Section titled “PaymentData”](#paymentdata)
Payment processing data including redirect information
| Field | Type | Description |
| ------------- | --------- | ------------------------------- |
| `redirectURL` | `String`! | URL to redirect the customer to |
## PaymentMethod
[Section titled “PaymentMethod”](#paymentmethod)
| Field | Type | Description |
| ------------ | -------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `extra` | [PaymentMethodExtra](/reference/graphql/unions/#paymentmethodextra) | |
| `id` | `ID`! | Unique identifier of the payment method |
| `internalId` | `String`! | Internal identifier of the payment method for the provider |
| `issuers` | \[[PaymentMethodOption](/reference/graphql/objects/#paymentmethodoption)!] | |
| `name` | `String`! | Human readable name of the payment method (localized) |
| `provider` | `String`! | Payment method provider |
## PaymentMethodApplePayConfiguration
[Section titled “PaymentMethodApplePayConfiguration”](#paymentmethodapplepayconfiguration)
| Field | Type | Description |
| -------------- | --------- | ----------- |
| `merchantId` | `String`! | |
| `merchantName` | `String`! | |
## PaymentMethodOption
[Section titled “PaymentMethodOption”](#paymentmethodoption)
| Field | Type | Description |
| ------ | --------- | ----------------------------------------------------- |
| `id` | `ID`! | Unique identifier of the payment method option |
| `name` | `String`! | Human readable name of the payment method (localized) |
## Product
[Section titled “Product”](#product)
Product entity containing all product information, variants, and metadata.
| Field | Type | Description |
| ------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `ID`! | Unique identifier for the product |
| `brand` | `String` | Brand of the product |
| `categories` | \[[ProductCategory](/reference/graphql/objects/#productcategory)!] | The categories to which the product belongs. |
| `image` | [ProductImage](/reference/graphql/objects/#productimage) | The primary image associated with the product. |
| `localizedSlugs` | \[[LocalizedSlug](/reference/graphql/objects/#localizedslug)!]! | Localized slugs for different locales *(args: `locales`)* |
| `meta` | [ResourceMeta](/reference/graphql/objects/#resourcemeta) | Metadata for the product |
| `name` | `String`! | Name of the product |
| `primaryCategory` | [ProductCategory](/reference/graphql/objects/#productcategory) | The primary category to which the product belongs. |
| `relatedProducts` | \[[Product](/reference/graphql/objects/#product)!] | Products that are similar. E.g. the same t-shirt in another color. |
| `reviews` | [Reviews](/reference/graphql/objects/#reviews) | Customer reviews for the product |
| `slug` | `String`! | URL-friendly slug for the product |
| `startingPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice) | The lowest price of all variants, often used to communicate the price on the product list page. |
| `variant` | [ProductVariant](/reference/graphql/objects/#productvariant)! | A specific variant, often used to show data in a product card. If no sku is given then the primary variant is returned. *(args: `sku`)* |
| `variantDimensions` | \[[ProductVariantDimension](/reference/graphql/objects/#productvariantdimension)!]! | Product variant dimensions for the product with selection options *(args: `selectedSku`)* |
| `variants` | \[[ProductVariant](/reference/graphql/objects/#productvariant)!]! | A list of all variants, often used to show all available options in a product detail page. *(args: `sku`)* |
| `articles` | \[[ContentPage](/reference/graphql/objects/#contentpage)!] | Related content articles for the product. |
| `breadcrumbs` | \[[Breadcrumb](/reference/graphql/objects/#breadcrumb)!] | |
## ProductBlock
[Section titled “ProductBlock”](#productblock)
Represents a row of product teasers.
| Field | Type | Description |
| ----------------- | ----------------------------------------------------------------- | -------------------------------------- |
| `id` | `ID`! | Unique identifier for the product row. |
| `productVariants` | \[[ProductVariant](/reference/graphql/objects/#productvariant)!]! | List of product variants in the row. |
| `title` | `String`! | Title of the product row. |
| `viewMoreLink` | [ResourceLink](/reference/graphql/objects/#resourcelink) | Link for viewing more products. |
## ProductCategoriesResult
[Section titled “ProductCategoriesResult”](#productcategoriesresult)
Paginated result containing product category pages
| Field | Type | Description |
| --------- | --------------------------------------------------------------------------- | ------------------------------------------------ |
| `results` | \[[ProductCategoryPage](/reference/graphql/objects/#productcategorypage)!]! | List of category pages in the result |
| `total` | `Int`! | Total number of categories matching the criteria |
## ProductCategory
[Section titled “ProductCategory”](#productcategory)
Represents a category of products.
| Field | Type | Description |
| ------------- | ------------------------------------------------------------------- | --------------------------------------------- |
| `id` | `ID`! | Unique identifier for the product category. |
| `key` | `String`! | Unique key for the product category |
| `ancestors` | \[[ProductCategory](/reference/graphql/objects/#productcategory)!]! | Ancestor categories in the category hierarchy |
| `description` | `String` | Description of the product category |
| `name` | `String`! | Name of the product category |
| `path` | `String`! | URL path for the category |
## ProductCategoryPage
[Section titled “ProductCategoryPage”](#productcategorypage)
Product category page containing products and metadata
| Field | Type | Description |
| ---------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `breadcrumbs` | \[[Breadcrumb](/reference/graphql/objects/#breadcrumb)!]! | The breadcrumbs representing the navigation path to the page. |
| `category` | [ProductCategory](/reference/graphql/objects/#productcategory)! | The product category associated with this page |
| `indexable` | `Boolean` | Indicates whether the page is indexable for search engines. |
| `meta` | [PageMeta](/reference/graphql/objects/#pagemeta) | Page metadata for SEO and other purposes |
| `name` | `String` | The name of the page. |
| `path` | `String`! | The URL path of the category page |
| `productListingConfig` | [ProductListingConfig](/reference/graphql/objects/#productlistingconfig)! | Configuration for the product listing on this page |
## ProductEntityReference
[Section titled “ProductEntityReference”](#productentityreference)
Reference to a product entity with basic information
| Field | Type | Description |
| ------ | -------- | ------------------------------------- |
| `name` | `String` | Name of the referenced product entity |
| `path` | `String` | Path to the referenced product entity |
## ProductImage
[Section titled “ProductImage”](#productimage)
Standard product image with URL and accessibility information
| Field | Type | Description |
| ------------- | --------- | ------------------------------ |
| `url` | `String`! | The URL to the image. |
| `alt` | `String` | Alternative text for the image |
| `description` | `String` | Description of the image |
## ProductImage360
[Section titled “ProductImage360”](#productimage360)
360-degree product image set for interactive viewing
| Field | Type | Description |
| ------------- | ------------- | --------------------------------------- |
| `alt` | `String` | Alternative text for the 360° image set |
| `description` | `String` | Description of the 360° image set |
| `urls` | \[`String`!]! | A list of URLs to the images. |
## ProductListingConfig
[Section titled “ProductListingConfig”](#productlistingconfig)
| Field | Type | Description |
| --------------- | ------------------------------------------------------------------ | ----------------------------------- |
| `categoryId` | `String`! | Category ID for the product listing |
| `defaultOrder` | [ProductSortOrder](/reference/graphql/enums/#productsortorder) | Default sort order for products |
| `enabledFacets` | \[`String`!] | List of enabled facet keys |
| `prefilters` | \[[FilterCondition](/reference/graphql/objects/#filtercondition)!] | Pre-applied filters for the listing |
## ProductRibbon
[Section titled “ProductRibbon”](#productribbon)
| Field | Type | Description |
| ------- | -------- | ------------------ |
| `kind` | `String` | NEW, SALE, etc. |
| `label` | `String` | Text in the ribbon |
## ProductSpecificationBooleanAttribute
[Section titled “ProductSpecificationBooleanAttribute”](#productspecificationbooleanattribute)
**Implements:** [AbstractProductSpecificationAttribute](/reference/graphql/interfaces/#abstractproductspecificationattribute)
| Field | Type | Description |
| ------------- | ---------- | ------------------------------------------------------------------------------------- |
| `description` | `String` | Optional descriptive information about the attribute, Localized to the current locale |
| `label` | `String`! | The label of the attribute, e.g. ‘Is fragile’. Localized to the current locale |
| `name` | `String`! | The name of the attribute, e.g. ‘isFragile’. Consistent across all locales |
| `value` | `Boolean`! | The boolean value of the attribute, e.g. true |
## ProductSpecificationDateTimeAttribute
[Section titled “ProductSpecificationDateTimeAttribute”](#productspecificationdatetimeattribute)
**Implements:** [AbstractProductSpecificationAttribute](/reference/graphql/interfaces/#abstractproductspecificationattribute)
| Field | Type | Description |
| ------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `description` | `String` | Optional descriptive information about the attribute, Localized to the current locale |
| `label` | `String`! | The label of the attribute, e.g. ‘Available from’. Localized to the current locale |
| `name` | `String`! | The name of the attribute, e.g. ‘availableFrom’. Consistent across all locales |
| `value` | [DateTime](/reference/graphql/scalars/#datetime)! | The datetime value of the attribute, e.g. 2024-08-14T07:52:15.520Z |
## ProductSpecificationEnumAttribute
[Section titled “ProductSpecificationEnumAttribute”](#productspecificationenumattribute)
**Implements:** [AbstractProductSpecificationAttribute](/reference/graphql/interfaces/#abstractproductspecificationattribute)
| Field | Type | Description |
| ------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `description` | `String` | Optional descriptive information about the attribute, Localized to the current locale |
| `label` | `String`! | The label of the attribute, e.g. ‘Season’. Localized to the current locale |
| `name` | `String`! | The name of the attribute, e.g. ‘season’. Consistent across all locales |
| `value` | [ProductSpecificationEnumValue](/reference/graphql/objects/#productspecificationenumvalue)! | The enum value of the attribute |
## ProductSpecificationEnumListAttribute
[Section titled “ProductSpecificationEnumListAttribute”](#productspecificationenumlistattribute)
**Implements:** [AbstractProductSpecificationAttribute](/reference/graphql/interfaces/#abstractproductspecificationattribute)
| Field | Type | Description |
| ------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `description` | `String` | Optional descriptive information about the attribute, Localized to the current locale |
| `label` | `String`! | The label of the attribute, e.g. ‘Audiences’. Localized to the current locale |
| `name` | `String`! | The name of the attribute, e.g. ‘audiences’. Consistent across all locales |
| `values` | \[[ProductSpecificationEnumValue](/reference/graphql/objects/#productspecificationenumvalue)]! | The enumc values of the attribute |
## ProductSpecificationEnumValue
[Section titled “ProductSpecificationEnumValue”](#productspecificationenumvalue)
| Field | Type | Description |
| ------- | --------- | ------------------------------------------------------------------ |
| `label` | `String`! | The label of the enum, e.g. ‘green’. Consistent across all locales |
| `value` | `String`! | The string value of the enum, e.g. ‘Green’ |
## ProductSpecificationGroup
[Section titled “ProductSpecificationGroup”](#productspecificationgroup)
| Field | Type | Description |
| ------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `attributes` | \[[ProductSpecificationAttribute](/reference/graphql/unions/#productspecificationattribute)!]! | Attributes for this specification group. These reflect a list of all the attributes associated with this specification group. |
| `description` | `String` | Optional descriptive information about the specification group, Localized to the current locale |
| `label` | `String`! | The label of the specification group, e.g. ‘Technical’. Localized to the current locale |
| `name` | `String`! | The name of the specification group, e.g. ‘technical’. Consistent across all locales |
## ProductSpecificationNumberAttribute
[Section titled “ProductSpecificationNumberAttribute”](#productspecificationnumberattribute)
**Implements:** [AbstractProductSpecificationAttribute](/reference/graphql/interfaces/#abstractproductspecificationattribute)
| Field | Type | Description |
| ------------- | --------- | ------------------------------------------------------------------------------------- |
| `description` | `String` | Optional descriptive information about the attribute, Localized to the current locale |
| `label` | `String`! | The label of the attribute, e.g. ‘Length’. Localized to the current locale |
| `name` | `String`! | The name of the attribute, e.g. ‘length’. Consistent across all locales |
| `value` | `Float`! | The integer value of the attribute, e.g. 1 |
## ProductSpecificationStringAttribute
[Section titled “ProductSpecificationStringAttribute”](#productspecificationstringattribute)
**Implements:** [AbstractProductSpecificationAttribute](/reference/graphql/interfaces/#abstractproductspecificationattribute)
| Field | Type | Description |
| ------------- | --------- | ------------------------------------------------------------------------------------- |
| `description` | `String` | Optional descriptive information about the attribute, Localized to the current locale |
| `label` | `String`! | The label of the attribute, e.g. ‘Size’. Localized to the current locale |
| `name` | `String`! | The name of the attribute, e.g. ‘size’. Consistent across all locales |
| `value` | `String`! | The string value of the attribute, e.g. ‘XL’. Localized to the current locale |
## ProductsResult
[Section titled “ProductsResult”](#productsresult)
Paginated result containing products and available facets for filtering
| Field | Type | Description |
| --------- | --------------------------------------------------- | ---------------------------------------------- |
| `facets` | \[[Facet](/reference/graphql/unions/#facet)!] | Available facets for filtering |
| `results` | \[[Product](/reference/graphql/objects/#product)!]! | List of products in the result |
| `total` | `Int`! | Total number of products matching the criteria |
## ProductVariant
[Section titled “ProductVariant”](#productvariant)
Specific variant of a product with unique SKU, pricing, and attributes
| Field | Type | Description |
| --------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `sku` | `String` | The SKU (stock keeping unit) for the variant. |
| `id` | `ID`! | Unique identifier for the variant |
| `name` | `String` | The name of the variant. |
| `primaryImage` | [ProductImage](/reference/graphql/objects/#productimage) | The main image associated with the product variant. |
| `availability` | [ProductAvailability](/reference/graphql/enums/#productavailability) | Availability status of the variant |
| `deliveryTime` | `String` | A description about when the product can be delivered. For example: Ordered |
| `description` | `String` | The variant description, can be formatted using Markdown. |
| `dimensions` | \[[Dimension](/reference/graphql/objects/#dimension)!]! | The dimensions specific to the product variant. Used to display the dimensions of the variant. |
| `ean` | `String` | European Article Number (EAN) |
| `highlightedSpecifications` | \[[ProductSpecificationAttribute](/reference/graphql/unions/#productspecificationattribute)!] | Highlighted specifications for this variant. These are a subset of the full specifications that are particularly |
| `hscode` | `String` | Harmonized System (HS) code for the variant. |
| `images` | \[[ProductImage](/reference/graphql/objects/#productimage)!] | All images associated with the product variant. |
| `listPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice) | Represents the list price, also known as te Manufacturer’s Suggested Retail |
| `price` | [TaxedPrice](/reference/graphql/objects/#taxedprice) | The price on the platform after regular product discounts are applied, it’s |
| `product` | [Product](/reference/graphql/objects/#product) | Backlink to the product. This is useful for the frontend to fetch product information if the entrypoint was the |
| `regularPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice) | The regular selling price on the platform, potentially already discounted |
| `ribbon` | [ProductRibbon](/reference/graphql/objects/#productribbon) | The ribbon. For example: ‘Boeket van de maand’ etc. |
| `specifications` | \[[ProductSpecificationGroup](/reference/graphql/objects/#productspecificationgroup)!] | Specifications for this variant. These reflect predefined groups of attributes that can be used to describe the |
## ProductVariantDimension
[Section titled “ProductVariantDimension”](#productvariantdimension)
Product variant dimension containing selectable options (e.g., size, color)
| Field | Type | Description |
| --------- | -------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `label` | `String`! | Display label for the dimension |
| `name` | `String`! | Internal name of the dimension |
| `options` | \[[ProductVariantDimensionOption](/reference/graphql/interfaces/#productvariantdimensionoption)!]! | Available options for this dimension |
## ProductVariantDimensionColorOption
[Section titled “ProductVariantDimensionColorOption”](#productvariantdimensioncoloroption)
Color-based product variant dimension option with hex color value
**Implements:** [ProductVariantDimensionOption](/reference/graphql/interfaces/#productvariantdimensionoption)
| Field | Type | Description |
| -------------- | -------------------------------------------------------------------- | -------------------------------------------------- |
| `availability` | [ProductAvailability](/reference/graphql/enums/#productavailability) | |
| `color` | `String`! | Hex value of the color used to display the option. |
| `label` | `String`! | |
| `product` | [Product](/reference/graphql/objects/#product) | |
| `selected` | `Boolean`! | |
| `value` | `String`! | |
| `variant` | [ProductVariant](/reference/graphql/objects/#productvariant) | |
## ProductVariantDimensionImageOption
[Section titled “ProductVariantDimensionImageOption”](#productvariantdimensionimageoption)
Image-based product variant dimension option (e.g., pattern, style)
**Implements:** [ProductVariantDimensionOption](/reference/graphql/interfaces/#productvariantdimensionoption)
| Field | Type | Description |
| -------------- | -------------------------------------------------------------------- | ----------- |
| `availability` | [ProductAvailability](/reference/graphql/enums/#productavailability) | |
| `label` | `String`! | |
| `product` | [Product](/reference/graphql/objects/#product) | |
| `selected` | `Boolean`! | |
| `value` | `String`! | |
| `variant` | [ProductVariant](/reference/graphql/objects/#productvariant) | |
## ProductVariantDimensionTextOption
[Section titled “ProductVariantDimensionTextOption”](#productvariantdimensiontextoption)
Text-based product variant dimension option (e.g., size, material)
**Implements:** [ProductVariantDimensionOption](/reference/graphql/interfaces/#productvariantdimensionoption)
| Field | Type | Description |
| -------------- | -------------------------------------------------------------------- | ----------- |
| `availability` | [ProductAvailability](/reference/graphql/enums/#productavailability) | |
| `label` | `String`! | |
| `product` | [Product](/reference/graphql/objects/#product) | |
| `selected` | `Boolean`! | |
| `value` | `String`! | |
| `variant` | [ProductVariant](/reference/graphql/objects/#productvariant) | |
## ProductYouTubeVideo
[Section titled “ProductYouTubeVideo”](#productyoutubevideo)
YouTube video embedded as product media
| Field | Type | Description |
| ------------- | --------- | ------------------------------ |
| `alt` | `String` | Alternative text for the video |
| `description` | `String` | Description of the video |
| `videoId` | `String`! | The YouTube video ID. |
## Quote
[Section titled “Quote”](#quote)
Quote provided by merchant in response to a quote request
| Field | Type | Description |
| --------------------- | --------------------------------------------------------------------------- | ------------------------------------------- |
| `id` | [QuoteIdentifier](/reference/graphql/objects/#quoteidentifier)! | Quote identifier containing id and version |
| `billingAddress` | [Address](/reference/graphql/objects/#address) | Customer billing address |
| `comment` | `String` | Comment from the customer |
| `createdAt` | [DateTime](/reference/graphql/scalars/#datetime)! | Creation date of the quote |
| `lineItems` | \[[QuoteSingleLineItem](/reference/graphql/objects/#quotesinglelineitem)!]! | Line items in the quote |
| `purchaseOrderNumber` | `String` | Purchase order number entered by the client |
| `shippingAddress` | [Address](/reference/graphql/objects/#address) | Customer shipping address |
| `status` | [QuoteStatus](/reference/graphql/enums/#quotestatus)! | Status of the quote |
| `total` | [TaxedTotalPrice](/reference/graphql/objects/#taxedtotalprice) | Sum of all line items |
## QuoteIdentifier
[Section titled “QuoteIdentifier”](#quoteidentifier)
Identifier for a quote with version control
| Field | Type | Description |
| --------- | ------ | ------------------------------------------------- |
| `id` | `ID`! | Unique identifier for the quote |
| `version` | `Int`! | Version number for optimistic concurrency control |
## QuoteRequest
[Section titled “QuoteRequest”](#quoterequest)
Quote request created by a customer
| Field | Type | Description |
| --------------------- | --------------------------------------------------------------------------- | ------------------------------------------- |
| `id` | [QuoteIdentifier](/reference/graphql/objects/#quoteidentifier)! | Quote identifier containing id and version |
| `billingAddress` | [Address](/reference/graphql/objects/#address) | Customer billing address |
| `comment` | `String` | Comment from the customer |
| `createdAt` | [DateTime](/reference/graphql/scalars/#datetime)! | Creation date of the quote |
| `lineItems` | \[[QuoteSingleLineItem](/reference/graphql/objects/#quotesinglelineitem)!]! | Line items in the quote |
| `purchaseOrderNumber` | `String` | Purchase order number entered by the client |
| `shippingAddress` | [Address](/reference/graphql/objects/#address) | Customer shipping address |
| `status` | [QuoteRequestStatus](/reference/graphql/enums/#quoterequeststatus)! | Status of the quote |
| `total` | [TaxedTotalPrice](/reference/graphql/objects/#taxedtotalprice) | Sum of all line items |
## QuoteRequestCreateResponse
[Section titled “QuoteRequestCreateResponse”](#quoterequestcreateresponse)
Response for quote request creation
| Field | Type | Description |
| -------------------- | ---------------------------------------------------------------------------------- | -------------------------------- |
| `quoteRequest` | [QuoteRequest](/reference/graphql/objects/#quoterequest)! | Quote request created |
| `quoteRequestErrors` | \[[QuoteRequestCreateError](/reference/graphql/unions/#quoterequestcreateerror)!]! | Error details if creation failed |
## QuoteRequestIdentifier
[Section titled “QuoteRequestIdentifier”](#quoterequestidentifier)
Identifier for a quote request with version control
| Field | Type | Description |
| --------- | ------ | ------------------------------------------------- |
| `id` | `ID`! | Unique identifier for the quote request |
| `version` | `Int`! | Version number for optimistic concurrency control |
## QuoteRequestResult
[Section titled “QuoteRequestResult”](#quoterequestresult)
Paginated result containing quote requests
| Field | Type | Description |
| --------- | ------------------------------------------------------------- | ----------------------------- |
| `results` | \[[QuoteRequest](/reference/graphql/objects/#quoterequest)!]! | List of quote requests |
| `total` | `Int`! | Total count of quote requests |
## QuoteResult
[Section titled “QuoteResult”](#quoteresult)
Paginated result containing quotes
| Field | Type | Description |
| --------- | ----------------------------------------------- | --------------------- |
| `results` | \[[Quote](/reference/graphql/objects/#quote)!]! | List of quotes |
| `total` | `Int`! | Total count of quotes |
## QuoteSingleLineItem
[Section titled “QuoteSingleLineItem”](#quotesinglelineitem)
Individual line item in a quote
**Implements:** [QuoteLineItem](/reference/graphql/interfaces/#quotelineitem)
| Field | Type | Description |
| -------------- | ------------------------------------------------------------- | ----------------------------------------- |
| `id` | `ID`! | Unique identifier for the line item |
| `listPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The list price (MSRP) |
| `name` | `String`! | Product name for the line item |
| `price` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The final price after applying discounts |
| `quantity` | `Int`! | Number of items |
| `regularPrice` | [TaxedPrice](/reference/graphql/objects/#taxedprice)! | The regular selling price on the platform |
| `variant` | [ProductVariant](/reference/graphql/objects/#productvariant)! | Product variant details |
## RangeFacet
[Section titled “RangeFacet”](#rangefacet)
Range-based facet for numeric filtering (e.g., price, rating)
| Field | Type | Description |
| ------------- | --------- | ------------------------------------------- |
| `key` | `String`! | Unique key for the facet (e.g., ‘price’) |
| `label` | `String`! | Display label for the facet (e.g., ‘Price’) |
| `max` | `Int`! | Maximum value in the range |
| `min` | `Int`! | Minimum value in the range |
| `selectedMax` | `Int` | Currently selected maximum value |
| `selectedMin` | `Int` | Currently selected minimum value |
## RefreshTokenResponse
[Section titled “RefreshTokenResponse”](#refreshtokenresponse)
| Field | Type | Description |
| -------------------- | ---------------------------------------------------------------------- | --------------------------------------------- |
| `refreshTokenErrors` | \[[RefreshTokenError](/reference/graphql/unions/#refreshtokenerror)!]! | Error details if token refresh failed |
| `success` | `Boolean` | Indicates if the token refresh was successful |
## ResourceLink
[Section titled “ResourceLink”](#resourcelink)
Represents a link to a resource, either internal or external.
| Field | Type | Description |
| -------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `anchor` | `String` | The anchor within the page or content that the link points to, if applicable. |
| `asset` | [ContentAsset](/reference/graphql/objects/#contentasset) | Asset for the link. |
| `id` | `ID` | CF entry id when this link is sourced from a real Contentful entry (the common case via `mapResourceLink`). Synthetic links built ad-hoc in resolvers (header / site config) leave this null. Authoring push relies on this to match an incoming link to its existing entry — without it every save creates a new Untitled draft and dangles the previous reference. |
| `name` | `String` | Name of the linked resource. |
| `newTab` | `Boolean` | Indicates if the link should open in a new tab. |
| `type` | [ResourceLinkType](/reference/graphql/enums/#resourcelinktype)! | Type of the resource link. |
| `value` | `String` | The URL or identifier for the content link, which can be a product ID, category ID, or page path. |
## ResourceMeta
[Section titled “ResourceMeta”](#resourcemeta)
Metadata for a resource
| Field | Type | Description |
| ------------- | -------- | -------------------- |
| `description` | `String` | Description metadata |
| `title` | `String` | Title metadata |
## Reviews
[Section titled “Reviews”](#reviews)
| Field | Type | Description |
| --------------- | -------- | -------------------------------------- |
| `amount` | `Int`! | The number of reviews for the product. |
| `averageRating` | `Float`! | The average rating for the product. |
## RichText
[Section titled “RichText”](#richtext)
Represents rich text content.
| Field | Type | Description |
| ---------- | -------------------------------------------------------------- | -------------------------------------------- |
| `content` | `String`! | Content of the rich text. |
| `id` | `ID`! | Unique identifier for the rich text content. |
| `renderer` | [RichTextRenderer](/reference/graphql/enums/#richtextrenderer) | Renderer used for the rich text content. |
## SetActiveBusinessContextResponse
[Section titled “SetActiveBusinessContextResponse”](#setactivebusinesscontextresponse)
| Field | Type | Description |
| -------------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `setActiveBusinessContextErrors` | \[[SetActiveBusinessContextError](/reference/graphql/unions/#setactivebusinesscontexterror)!]! | Error details if setting active business context failed |
| `success` | `Boolean` | Indicates if setting active business context was successful |
## ShippingMethod
[Section titled “ShippingMethod”](#shippingmethod)
| Field | Type | Description |
| ------------- | ------------------------------------------- | ---------------------------------------- |
| `description` | `String` | Description of the shipping method |
| `id` | `ID`! | Unique identifier of the shipping method |
| `key` | `String`! | Unique key of the shipping method |
| `name` | `String`! | Name of the shipping method |
| `price` | [Money](/reference/graphql/objects/#money)! | Price of the shipping method. |
## ShoppingList
[Section titled “ShoppingList”](#shoppinglist)
Customer shopping list containing saved products for future purchase
| Field | Type | Description |
| ------------- | ----------------------------------------------------------------------------- | ----------------------------------------- |
| `id` | `ID`! | Unique identifier for the shopping list |
| `description` | `String` | Optional description of the shopping list |
| `items` | \[[ShoppingListLineItem](/reference/graphql/objects/#shoppinglistlineitem)!]! | Items in the shopping list |
| `name` | `String`! | Name of the shopping list |
## ShoppingListCreateResponse
[Section titled “ShoppingListCreateResponse”](#shoppinglistcreateresponse)
| Field | Type | Description |
| -------------------- | ---------------------------------------------------------------------------------- | --------------------------------- |
| `shoppingList` | [ShoppingList](/reference/graphql/objects/#shoppinglist) | Created shopping list information |
| `shoppingListErrors` | \[[ShoppingListCreateError](/reference/graphql/unions/#shoppinglistcreateerror)!]! | Error details if creation failed |
## ShoppingListDeleteResponse
[Section titled “ShoppingListDeleteResponse”](#shoppinglistdeleteresponse)
| Field | Type | Description |
| -------------------- | ---------------------------------------------------------------------------------- | -------------------------------- |
| `shoppingListErrors` | \[[ShoppingListDeleteError](/reference/graphql/unions/#shoppinglistdeleteerror)!]! | Error details if deletion failed |
| `success` | `Boolean`! | |
## ShoppingListLineItem
[Section titled “ShoppingListLineItem”](#shoppinglistlineitem)
Individual item within a shopping list containing product and quantity information
| Field | Type | Description |
| ---------- | ------------------------------------------------------------- | ----------------------------------------- |
| `id` | `ID`! | Unique identifier for the line item |
| `quantity` | `Int`! | Quantity of the item in the shopping list |
| `slug` | `String` | URL slug for the product (optional) |
| `variant` | [ProductVariant](/reference/graphql/objects/#productvariant)! | Product variant for this line item |
## ShoppingListLineItemAddResponse
[Section titled “ShoppingListLineItemAddResponse”](#shoppinglistlineitemaddresponse)
| Field | Type | Description |
| -------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `shoppingList` | [ShoppingList](/reference/graphql/objects/#shoppinglist) | Updated shopping list information after line item operation |
| `shoppingListErrors` | \[[ShoppingListLineItemAddError](/reference/graphql/unions/#shoppinglistlineitemadderror)!]! | Error details if line item operation failed |
## ShoppingListLineItemRemoveResponse
[Section titled “ShoppingListLineItemRemoveResponse”](#shoppinglistlineitemremoveresponse)
| Field | Type | Description |
| -------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------- |
| `shoppingList` | [ShoppingList](/reference/graphql/objects/#shoppinglist) | Updated shopping list information after line item operation |
| `shoppingListErrors` | \[[ShoppingListLineRemoveError](/reference/graphql/unions/#shoppinglistlineremoveerror)!]! | Error details if line item operation failed |
## ShoppingListLineItemUpdateResponse
[Section titled “ShoppingListLineItemUpdateResponse”](#shoppinglistlineitemupdateresponse)
| Field | Type | Description |
| -------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------- |
| `shoppingList` | [ShoppingList](/reference/graphql/objects/#shoppinglist) | Updated shopping list information after line item operation |
| `shoppingListErrors` | \[[ShoppingListLineUpdateError](/reference/graphql/unions/#shoppinglistlineupdateerror)!]! | Error details if line item operation failed |
## ShoppingListResult
[Section titled “ShoppingListResult”](#shoppinglistresult)
Paginated result containing shopping lists and metadata
| Field | Type | Description |
| --------- | ------------------------------------------------------------- | --------------------------------------------------------- |
| `results` | \[[ShoppingList](/reference/graphql/objects/#shoppinglist)!]! | List of shopping lists returned in the result |
| `total` | `Int`! | Total count of shopping lists matching the query criteria |
## ShoppingListUpdateResponse
[Section titled “ShoppingListUpdateResponse”](#shoppinglistupdateresponse)
| Field | Type | Description |
| -------------------- | ---------------------------------------------------------------------------------- | --------------------------------- |
| `shoppingList` | [ShoppingList](/reference/graphql/objects/#shoppinglist) | Updated shopping list information |
| `shoppingListErrors` | \[[ShoppingListUpdateError](/reference/graphql/unions/#shoppinglistupdateerror)!]! | Error details if update failed |
## Site
[Section titled “Site”](#site)
Represents a website with various components and settings.
| Field | Type | Description |
| ---------- | ---------------------------------------------------- | ------------------------------- |
| `id` | `ID`! | Unique identifier for the site. |
| `mainMenu` | \[[MenuItem](/reference/graphql/objects/#menuitem)!] | Main menu items for the site. |
## SiteFooter
[Section titled “SiteFooter”](#sitefooter)
| Field | Type | Description |
| ---------------- | ------------------------------------------------------------ | ------------------------------------------- |
| `bottomBarLinks` | \[[ResourceLink](/reference/graphql/objects/#resourcelink)!] | Bottom links for the footer. |
| `footerMenu` | \[[MenuBlock](/reference/graphql/objects/#menublock)!] | Footer menu blocks for the site. |
| `paymentMethods` | \[[ResourceLink](/reference/graphql/objects/#resourcelink)!] | Payment method assets for the site. |
| `socials` | \[[ResourceLink](/reference/graphql/objects/#resourcelink)!] | Social media links with icons for the site. |
## SiteHeader
[Section titled “SiteHeader”](#siteheader)
| Field | Type | Description |
| ------------- | ------------------------------------------------------------ | ------------------------------------------- |
| `id` | `ID`! | Unique identifier for the header. |
| `logo` | [ContentAsset](/reference/graphql/objects/#contentasset) | Logo asset for the header. |
| `siteRating` | [SiteRating](/reference/graphql/objects/#siterating) | Site rating for the header. |
| `topBarLinks` | \[[ResourceLink](/reference/graphql/objects/#resourcelink)!] | Top bar links for the header. |
| `usps` | \[[Usp](/reference/graphql/objects/#usp)!]! | USPs (Unique Selling Points) in the header. |
## SiteLayout
[Section titled “SiteLayout”](#sitelayout)
Site layout configuration including header, footer, and navigation
| Field | Type | Description |
| ------------ | ------------------------------------------------------------ | ------------------------------------------ |
| `footer` | [SiteFooter](/reference/graphql/objects/#sitefooter) | Footer configuration for the site |
| `header` | [SiteHeader](/reference/graphql/objects/#siteheader) | Header configuration for the site |
| `navigation` | [NavigationMenu](/reference/graphql/objects/#navigationmenu) | Navigation menu configuration for the site |
## SiteRating
[Section titled “SiteRating”](#siterating)
| Field | Type | Description |
| ------- | -------------------------------------------------------- | ------------------------------- |
| `count` | `Int`! | Number of reviews for the site. |
| `link` | [ResourceLink](/reference/graphql/objects/#resourcelink) | Review site link for the site. |
| `value` | `Float`! | Average rating for the site. |
## Socials
[Section titled “Socials”](#socials)
Represents social media links for a website.
| Field | Type | Description |
| ----------- | -------- | ---------------------- |
| `facebook` | `String` | Facebook profile URL. |
| `instagram` | `String` | Instagram profile URL. |
| `linkedIn` | `String` | LinkedIn profile URL. |
| `twitter` | `String` | Twitter profile URL. |
| `youtube` | `String` | YouTube channel URL. |
## Store
[Section titled “Store”](#store)
Physical or virtual store location within a business unit
| Field | Type | Description |
| ------ | --------- | ------------------------------- |
| `id` | `ID`! | Unique identifier for the store |
| `name` | `String`! | Name of the store |
## StoreContentPreviewData
[Section titled “StoreContentPreviewData”](#storecontentpreviewdata)
Represents data for storing content previews.
| Field | Type | Description |
| ----- | ---- | ----------------------------------------------- |
| `id` | `ID` | Identifier for the stored content preview data. |
## StoreContentPreviewResponse
[Section titled “StoreContentPreviewResponse”](#storecontentpreviewresponse)
| Field | Type | Description |
| --------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------- |
| `data` | [StoreContentPreviewData](/reference/graphql/objects/#storecontentpreviewdata) | Response containing the stored content preview data. |
| `previewErrors` | \[[Error](/reference/graphql/interfaces/#error)!] | List of errors encountered while storing the content preview. |
## StoreContext
[Section titled “StoreContext”](#storecontext)
| Field | Type | Description |
| ---------- | --------- | ---------------------------------------------------- |
| `currency` | `String`! | The currency used in the store context. |
| `locale` | `String`! | The locale of the store context. For example: en-US. |
| `storeKey` | `String`! | The store key associated with the store context. |
## Tax
[Section titled “Tax”](#tax)
| Field | Type | Description |
| ------- | ------------------------------------------- | ---------------------------------------------------- |
| `rate` | `Float`! | Rate of the tax line item as percentage from 0 to 1. |
| `value` | [Money](/reference/graphql/objects/#money)! | Value in money of the tax line item. |
## TaxedPrice
[Section titled “TaxedPrice”](#taxedprice)
| Field | Type | Description |
| ------- | ------------------------------------------- | ----------- |
| `gross` | [Money](/reference/graphql/objects/#money)! | |
| `net` | [Money](/reference/graphql/objects/#money)! | |
| `tax` | [Tax](/reference/graphql/objects/#tax)! | |
## TaxedTotalPrice
[Section titled “TaxedTotalPrice”](#taxedtotalprice)
A total price consists of items that can have different tax rates.
| Field | Type | Description |
| ------- | ------------------------------------------- | ----------- |
| `gross` | [Money](/reference/graphql/objects/#money)! | |
| `net` | [Money](/reference/graphql/objects/#money)! | |
| `taxes` | \[[Tax](/reference/graphql/objects/#tax)!]! | |
## Teaser
[Section titled “Teaser”](#teaser)
Represents a single teaser item.
| Field | Type | Description |
| --------- | --------------------------------------------------------- | --------------------------------- |
| `content` | `String`! | Content of the teaser. |
| `id` | `ID`! | Unique identifier for the teaser. |
| `image` | [ContentAsset](/reference/graphql/objects/#contentasset)! | Image asset for the teaser. |
| `link` | [ResourceLink](/reference/graphql/objects/#resourcelink)! | Link associated with the teaser. |
| `title` | `String`! | Title of the teaser. |
## TeaserBlock
[Section titled “TeaserBlock”](#teaserblock)
Represents a content teaser with additional styling options.
| Field | Type | Description |
| --------------- | --------------------------------------------------------- | -------------------------------------------- |
| `content` | `String`! | Content of the teaser. |
| `id` | `ID`! | Unique identifier for the content teaser. |
| `image` | [ContentAsset](/reference/graphql/objects/#contentasset)! | Image asset for the content teaser. |
| `imagePosition` | [ImagePosition](/reference/graphql/enums/#imageposition)! | Position of the image in the content teaser. |
| `link` | [ResourceLink](/reference/graphql/objects/#resourcelink)! | Link associated with the content teaser. |
| `title` | `String`! | Title of the content teaser. |
## TeasersBlock
[Section titled “TeasersBlock”](#teasersblock)
Represents a row of teasers.
| Field | Type | Description |
| -------------- | -------------------------------------------------------- | --------------------------------------------------------------------------- |
| `id` | `ID`! | Unique identifier for the teaser row. |
| `items` | \[[Teaser](/reference/graphql/objects/#teaser)!]! | List of teasers in the row. |
| `title` | `String`! | Title of the teaser row. |
| `viewMoreLink` | [ResourceLink](/reference/graphql/objects/#resourcelink) | Link for viewing more content. If empty, no view more button will be shown. |
## ThreeColumnsBlock
[Section titled “ThreeColumnsBlock”](#threecolumnsblock)
Represents a three-column layout structure.
| Field | Type | Description |
| --------- | ---------------------------------------------------------- | ---------------------------------------------- |
| `column1` | \[[ColumnEntry](/reference/graphql/unions/#columnentry)!]! | Content blocks for the first column. |
| `column2` | \[[ColumnEntry](/reference/graphql/unions/#columnentry)!]! | Content blocks for the second column. |
| `column3` | \[[ColumnEntry](/reference/graphql/unions/#columnentry)!]! | Content blocks for the third column. |
| `id` | `ID`! | Unique identifier for the three-column layout. |
## ToggleFacet
[Section titled “ToggleFacet”](#togglefacet)
Boolean toggle facet for binary filtering (e.g., in stock, on sale)
| Field | Type | Description |
| --------- | --------------------------------------------------------- | ------------------------------------------------- |
| `key` | `String`! | Unique key for the facet (e.g., ‘sustainable’) |
| `label` | `String`! | Display label for the facet (e.g., ‘Sustainable’) |
| `options` | \[[FacetValue](/reference/graphql/objects/#facetvalue)!]! | Toggle options (exactly 2: true and false) |
## TranslatedPath
[Section titled “TranslatedPath”](#translatedpath)
Represents a translated path for a specific locale.
| Field | Type | Description |
| -------- | --------- | ----------------------------------------- |
| `locale` | `String`! | The locale of the translated path. |
| `path` | `String`! | The translated path for the given locale. |
## TwoColumnsBlock
[Section titled “TwoColumnsBlock”](#twocolumnsblock)
Represents a two-column layout structure.
| Field | Type | Description |
| --------- | ------------------------------------------------------------- | -------------------------------------------- |
| `column1` | \[[ColumnEntry](/reference/graphql/unions/#columnentry)!]! | Content blocks for the first column. |
| `column2` | \[[ColumnEntry](/reference/graphql/unions/#columnentry)!]! | Content blocks for the second column. |
| `id` | `ID`! | Unique identifier for the two-column layout. |
| `ratio` | [TwoColumnsRatio](/reference/graphql/enums/#twocolumnsratio)! | Ratio of the two columns. |
## Usp
[Section titled “Usp”](#usp)
Represents a single Unique Selling Point (USP).
| Field | Type | Description |
| ------- | -------------------------------------------------------- | ----------------------------------------------------- |
| `id` | `ID`! | Unique identifier for the USP (Unique Selling Point). |
| `link` | [ResourceLink](/reference/graphql/objects/#resourcelink) | Optional link associated with the USP. |
| `title` | `String`! | Title of the USP. |
## UspBlock
[Section titled “UspBlock”](#uspblock)
Represents a list of Unique Selling Points (USPs).
| Field | Type | Description |
| -------- | ---------------------------------------------------------- | ------------------------------------------------------- |
| `format` | [UspBlockFormat](/reference/graphql/enums/#uspblockformat) | Format of the USP list, e.g., ‘horizontal’, ‘vertical’. |
| `id` | `ID`! | Unique identifier for the USP list. |
| `items` | \[[Usp](/reference/graphql/objects/#usp)!]! | List of USPs. |
## Usps
[Section titled “Usps”](#usps)
Represents a group of Unique Selling Points (USPs).
| Field | Type | Description |
| ------ | --------- | --------------------------------------------- |
| `id` | `ID`! | Unique identifier for the USPs group. |
| `usp1` | `String`! | First USP (Unique Selling Point). |
| `usp2` | `String`! | Second USP (Unique Selling Point). |
| `usp3` | `String`! | Third USP (Unique Selling Point). |
| `usp4` | `String` | Fourth USP (Unique Selling Point) (optional). |
## ValidateApplePayPaymentResponse
[Section titled “ValidateApplePayPaymentResponse”](#validateapplepaypaymentresponse)
| Field | Type | Description |
| ---------------- | -------------------------------------------------------------------------------------------- | ---------------------------------- |
| `applePayErrors` | \[[ValidateApplePayPaymentError](/reference/graphql/unions/#validateapplepaypaymenterror)!]! | Error details if validation failed |
| `data` | `String` | Encrypted Apple Pay payment data |
## ValidationError
[Section titled “ValidationError”](#validationerror)
**Implements:** [Error](/reference/graphql/interfaces/#error)
| Field | Type | Description |
| --------- | ----------------------------------------------------------------------------- | ----------- |
| `fields` | \[[FieldValidationIssue](/reference/graphql/objects/#fieldvalidationissue)!]! | |
| `message` | `String`! | |
## YoutubeVideo
[Section titled “YoutubeVideo”](#youtubevideo)
Represents a video with its associated content.
| Field | Type | Description |
| ----------- | ---------- | -------------------------------------------------------------------- |
| `autoplay` | `Boolean`! | Whether the video should autoplay when loaded. |
| `controls` | `Boolean`! | Whether the video should show controls like play/pause, volume, etc. |
| `id` | `ID`! | Unique identifier for the video. |
| `loop` | `Boolean`! | Whether the video should loop when it reaches the end. |
| `youtubeId` | `String`! | The YouTube video ID. |
# Queries
> All read operations exposed by the Evolve GraphQL gateway.
All read operations exposed by the Evolve GraphQL gateway.
## businessUnit
[Section titled “businessUnit”](#businessunit)
```graphql
businessUnit(
id: ID!
): BusinessUnit
```
Get a specific business unit by ID
Example:
```graphql
query {
businessUnit(id: "bu-123") {
id
name
key
addresses {
id
firstName
lastName
city
}
associates {
id
email
roles {
id
key
}
}
}
}
```
**Returns:** [BusinessUnit](/reference/graphql/objects/#businessunit)
**Arguments**
* `id`: `ID`!
## businessUnits
[Section titled “businessUnits”](#businessunits)
```graphql
businessUnits: [BusinessUnit!]!
```
Get all business units accessible to the current user
Example:
```graphql
query {
businessUnits {
id
name
key
stores {
id
key
name
}
}
}
```
**Returns:** \[[BusinessUnit](/reference/graphql/objects/#businessunit)!]!
## customer
[Section titled “customer”](#customer)
```graphql
customer: Customer
```
Get the current authenticated customer’s information
Example:
```graphql
query {
customer {
id
email
givenName
familyName
addresses {
id
firstName
lastName
city
}
}
}
```
**Returns:** [Customer](/reference/graphql/objects/#customer)
## customerExists
[Section titled “customerExists”](#customerexists)
```graphql
customerExists(
email: EmailAddress!
): CustomerExistsResult!
```
Check if a customer account exists for the given email address
Example:
```graphql
query {
customerExists(email: "john.doe@example.com") {
exists
}
}
```
**Returns:** [CustomerExistsResult](/reference/graphql/objects/#customerexistsresult)!
**Arguments**
* `email`: [EmailAddress](/reference/graphql/scalars/#emailaddress)!
## shoppingList
[Section titled “shoppingList”](#shoppinglist)
```graphql
shoppingList(
id: ID
): ShoppingList
```
Get a specific shopping list by ID (returns default list if no ID provided)
Example:
```graphql
query {
shoppingList(id: "sl-123") {
id
name
description
lineItems {
id
sku
quantity
productVariant {
id
sku
}
}
}
}
```
**Returns:** [ShoppingList](/reference/graphql/objects/#shoppinglist)
**Arguments**
* `id`: `ID`
## shoppingLists
[Section titled “shoppingLists”](#shoppinglists)
```graphql
shoppingLists(
page: Int
pageSize: Int
): ShoppingListResult
```
Get paginated list of shopping lists for the current user
Example:
```graphql
query {
shoppingLists(page: 1, pageSize: 20) {
items {
id
name
description
lineItemsCount
}
meta {
page
pageSize
totalItems
totalPages
}
}
}
```
**Returns:** [ShoppingListResult](/reference/graphql/objects/#shoppinglistresult)
**Arguments**
* `page`: `Int`
* `pageSize`: `Int`
## categories
[Section titled “categories”](#categories)
```graphql
categories(
page: Int!
pageSize: Int!
searchTerm: String
sort: CategorySortOrder
): ProductCategoriesResult!
```
Get a paginated list of product categories with optional search and sorting
Example:
```graphql
query {
categories(
page: 1
pageSize: 50
searchTerm: "electronics"
sort: NAME_ASC
) {
items {
id
key
name
slug
description
parent {
id
name
}
ancestors {
id
name
}
}
meta {
page
pageSize
totalItems
}
}
}
```
**Returns:** [ProductCategoriesResult](/reference/graphql/objects/#productcategoriesresult)!
**Arguments**
* `page`: `Int`!
* `pageSize`: `Int`!
* `searchTerm`: `String`
* `sort`: [CategorySortOrder](/reference/graphql/enums/#categorysortorder)
## product
[Section titled “product”](#product)
```graphql
product(
slug: String!
): Product
```
Get a single product by its slug
Example:
```graphql
query {
product(slug: "laptop-dell-xps-13") {
id
slug
name
description
masterVariant {
id
sku
price {
value
currencyCode
}
images {
url
alt
}
}
variants {
id
sku
attributes {
name
value
}
}
}
}
```
**Returns:** [Product](/reference/graphql/objects/#product)
**Arguments**
* `slug`: `String`!
## productCategoryPageByPath
[Section titled “productCategoryPageByPath”](#productcategorypagebypath)
```graphql
productCategoryPageByPath(
path: String!
): ProductCategoryPage
```
Get category page data with associated products and CMS content by path
Example:
```graphql
query {
productCategoryPageByPath(path: "/categories/electronics/laptops") {
category {
id
name
description
}
products {
items {
id
name
masterVariant {
id
sku
price {
value
currencyCode
}
}
}
}
}
}
```
**Returns:** [ProductCategoryPage](/reference/graphql/objects/#productcategorypage)
**Arguments**
* `path`: `String`!
## productSearch
[Section titled “productSearch”](#productsearch)
```graphql
productSearch(
categoryId: String
filters: [FacetFilterInput!]
page: Int!
pageSize: Int!
prefilters: [FilterConditionInput!]
searchTerm: String
sort: ProductSortOrder!
): ProductsResult!
```
Search for products with advanced filtering, sorting, and pagination capabilities
Example:
```graphql
query {
productSearch(
searchTerm: "laptop"
categoryId: "cat-electronics"
page: 1
pageSize: 24
sort: RELEVANCE
filters: [
{
field: "price"
range: { min: 500, max: 2000 }
},
{
field: "brand"
values: ["Apple", "Dell"]
}
]
) {
items {
id
slug
name
description
masterVariant {
id
sku
price {
value
currencyCode
}
}
}
facets {
field
count
... on RangeFacet {
min
max
}
... on OptionsFacet {
options {
value
count
}
}
}
meta {
page
pageSize
totalItems
}
}
}
```
**Returns:** [ProductsResult](/reference/graphql/objects/#productsresult)!
**Arguments**
* `categoryId`: `String`
* `filters`: \[[FacetFilterInput](/reference/graphql/inputs/#facetfilterinput)!]
* `page`: `Int`!
* `pageSize`: `Int`!
* `prefilters`: \[[FilterConditionInput](/reference/graphql/inputs/#filterconditioninput)!]
* `searchTerm`: `String`
* `sort`: [ProductSortOrder](/reference/graphql/enums/#productsortorder)!
## products
[Section titled “products”](#products)
```graphql
products(
page: Int!
pageSize: Int!
): ProductsResult!
```
Get a paginated list of all products without search or filtering
Example:
```graphql
query {
products(page: 1, pageSize: 50) {
items {
id
slug
name
masterVariant {
id
sku
price {
value
currencyCode
}
}
}
meta {
page
pageSize
totalItems
}
}
}
```
**Returns:** [ProductsResult](/reference/graphql/objects/#productsresult)!
**Arguments**
* `page`: `Int`!
* `pageSize`: `Int`!
## productsBySKU
[Section titled “productsBySKU”](#productsbysku)
```graphql
productsBySKU(
skus: [String!]!
): [Product!]!
```
Retrieve specific products by their SKU values
Example:
```graphql
query {
productsBySKU(skus: ["SKU-001", "SKU-002", "SKU-003"]) {
id
slug
name
masterVariant {
id
sku
price {
value
currencyCode
}
images {
url
alt
}
}
}
}
```
**Returns:** \[[Product](/reference/graphql/objects/#product)!]!
**Arguments**
* `skus`: \[`String`!]!
## addressValidate
[Section titled “addressValidate”](#addressvalidate)
```graphql
addressValidate(
address: AddressInput!
): AddressValidationResult
```
Validate a shipping or billing address for correctness and formatting
Example:
```graphql
query {
addressValidate(address: {
firstName: "John"
lastName: "Doe"
streetName: "Main Street"
streetNumber: "123"
city: "Amsterdam"
postalCode: "1000AA"
country: "NL"
}) {
isValid
errors {
field
message
}
suggestions {
streetName
city
postalCode
}
}
}
```
**Returns:** [AddressValidationResult](/reference/graphql/objects/#addressvalidationresult)
**Arguments**
* `address`: [AddressInput](/reference/graphql/inputs/#addressinput)!
## cart
[Section titled “cart”](#cart)
```graphql
cart: Cart
```
Get the current user’s shopping cart with all line items and totals
Example:
```graphql
query {
cart {
id
version
lineItems {
... on CartSingleLineItem {
id
quantity
totalPrice {
value
currencyCode
}
productVariant {
id
sku
}
}
}
totalPrice {
value
currencyCode
}
shippingAddress {
firstName
lastName
city
}
billingAddress {
firstName
lastName
city
}
}
}
```
**Returns:** [Cart](/reference/graphql/objects/#cart)
## authoringBlockById
[Section titled “authoringBlockById”](#authoringblockbyid)
```graphql
authoringBlockById(
id: ID!
): String
```
Fetch a single CMS entry by id and return it in gateway shape (with all linked entries and assets resolved). Used by the editor’s library drawer to preview / drag-drop existing entries onto a draft page. The entry is JSON-encoded — string + parse over a custom scalar to keep the subgraph dependency-light, same approach as `authoringPushPage`. Returns `null` if not found.
**Returns:** `String`
**Arguments**
* `id`: `ID`!
## authoringListPages
[Section titled “authoringListPages”](#authoringlistpages)
```graphql
authoringListPages(
locale: String
): [AuthoringPageSummary!]!
```
Authoring-side page list. Surfaces editor-specific fields the public `pages` query doesn’t (draft/changed/published status, last editor’s name) by hitting the Management API directly with the editor user’s OAuth token. Used by the drawer’s Pages tab.
**Returns:** \[[AuthoringPageSummary](/reference/graphql/objects/#authoringpagesummary)!]!
**Arguments**
* `locale`: `String` — Optional locale filter — returns only pages with content in this locale.
## authoringPushLocales
[Section titled “authoringPushLocales”](#authoringpushlocales)
```graphql
authoringPushLocales: AuthoringPushLocales!
```
Pre-flight info for the push dialog: which locales the next push will write to (storefront request locale + CF space default). The dialog shows this so the editor knows what’s about to land where.
**Returns:** [AuthoringPushLocales](/reference/graphql/objects/#authoringpushlocales)!
## authoringSearchAssets
[Section titled “authoringSearchAssets”](#authoringsearchassets)
```graphql
authoringSearchAssets(
limit: Int!
query: String
): AuthoringAssetSearchResult!
```
Authoring-side asset library search. Substring + case-insensitive full-text search over CMS image assets. Used by the editor’s library drawer and AI tooling.
**Returns:** [AuthoringAssetSearchResult](/reference/graphql/objects/#authoringassetsearchresult)!
**Arguments**
* `limit`: `Int`!
* `query`: `String`
## authoringSearchEntries
[Section titled “authoringSearchEntries”](#authoringsearchentries)
```graphql
authoringSearchEntries(
contentType: String
excludedContentTypes: [String!]
limit: Int!
query: String
): AuthoringEntrySearchResult!
```
Authoring-side entry library search. Substring + case-insensitive full-text search over CMS entries, optionally narrowed by content type id. Used by the editor’s library drawer and AI tooling.
**Returns:** [AuthoringEntrySearchResult](/reference/graphql/objects/#authoringentrysearchresult)!
**Arguments**
* `contentType`: `String`
* `excludedContentTypes`: \[`String`!]
* `limit`: `Int`!
* `query`: `String`
## catalogPage
[Section titled “catalogPage”](#catalogpage)
```graphql
catalogPage(
path: String!
previewID: String
version: ContentVersion
): CatalogPage
```
Fetch a catalog page with product browsing and filtering capabilities
Example:
```graphql
query {
catalogPage(
path: "/categories/laptops"
version: PUBLISHED
) {
id
path
title
category {
id
name
description
}
filters {
field
type
label
}
sortOptions {
value
label
}
}
}
```
**Returns:** [CatalogPage](/reference/graphql/objects/#catalogpage)
**Arguments**
* `path`: `String`!
* `previewID`: `String`
* `version`: [ContentVersion](/reference/graphql/enums/#contentversion)
## contentPage
[Section titled “contentPage”](#contentpage)
```graphql
contentPage(
path: String!
previewID: String
version: ContentVersion
): ContentPage
```
Fetch a content page by path with optional preview functionality Used for CMS content pages like About Us, Terms & Conditions, etc.
Example:
```graphql
query {
contentPage(
path: "/about-us"
version: PUBLISHED
previewID: "preview-123"
) {
id
path
title
content {
... on TextBlock {
text
}
... on ImageBlock {
image {
url
alt
}
}
}
seo {
title
description
}
}
}
```
**Returns:** [ContentPage](/reference/graphql/objects/#contentpage)
**Arguments**
* `path`: `String`!
* `previewID`: `String`
* `version`: [ContentVersion](/reference/graphql/enums/#contentversion)
## contentSnippet
[Section titled “contentSnippet”](#contentsnippet)
```graphql
contentSnippet(
key: String!
previewID: String
version: ContentVersion
): ContentSnippet
```
Get a specific content snippet by key. These snippets are reusable content blocks that can be used accross different pages, and allow for more flexibility in content management.
**Returns:** [ContentSnippet](/reference/graphql/objects/#contentsnippet)
**Arguments**
* `key`: `String`!
* `previewID`: `String`
* `version`: [ContentVersion](/reference/graphql/enums/#contentversion)
## page
[Section titled “page”](#page)
```graphql
page(
path: String!
previewID: String
version: ContentVersion
): Page
```
Fetch any page type (content or product listing) by path Generic page resolver that returns the appropriate page type
Example:
```graphql
query {
page(path: "/contact", version: PUBLISHED) {
... on ContentPage {
id
title
content {
... on TextBlock {
text
}
}
}
... on CatalogPage {
id
title
category {
name
}
}
}
}
```
**Returns:** [Page](/reference/graphql/unions/#page)
**Arguments**
* `path`: `String`!
* `previewID`: `String`
* `version`: [ContentVersion](/reference/graphql/enums/#contentversion)
## pages
[Section titled “pages”](#pages)
```graphql
pages(
page: Int!
pageSize: Int!
searchTerm: String
sort: PageSortOrder
types: [PageType!]
version: ContentVersion
): PagesResult!
```
Get a paginated list of pages with filtering and search capabilities Useful for sitemaps, navigation, and content management
Example:
```graphql
query {
pages(
page: 1
pageSize: 50
version: PUBLISHED
types: [CONTENT_PAGE, CATALOG_PAGE]
searchTerm: "electronics"
sort: nameAscending
) {
items {
... on ContentPage {
id
path
title
}
... on CatalogPage {
id
path
title
category {
name
}
}
}
meta {
page
pageSize
totalItems
}
}
}
```
**Returns:** [PagesResult](/reference/graphql/objects/#pagesresult)!
**Arguments**
* `page`: `Int`!
* `pageSize`: `Int`!
* `searchTerm`: `String`
* `sort`: [PageSortOrder](/reference/graphql/enums/#pagesortorder)
* `types`: \[[PageType](/reference/graphql/enums/#pagetype)!]
* `version`: [ContentVersion](/reference/graphql/enums/#contentversion)
## site
[Section titled “site”](#site)
```graphql
site(
previewID: String
version: ContentVersion
): Site
```
Get site-wide information and configuration Used for global site settings, branding, and configuration
Example:
```graphql
query {
site(version: PUBLISHED) {
id
name
description
logo {
url
alt
}
favicon {
url
}
social {
facebook
twitter
instagram
}
}
}
```
**Returns:** [Site](/reference/graphql/objects/#site)
**Arguments**
* `previewID`: `String`
* `version`: [ContentVersion](/reference/graphql/enums/#contentversion)
## siteLayout
[Section titled “siteLayout”](#sitelayout)
```graphql
siteLayout(
previewID: String
version: ContentVersion
): SiteLayout
```
Get site layout configuration including header, footer, and navigation Used for consistent site-wide layout and navigation structure
Example:
```graphql
query {
siteLayout(version: PUBLISHED) {
header {
logo {
url
alt
}
navigation {
title
url
children {
title
url
}
}
}
footer {
copyright
links {
title
url
}
}
}
}
```
**Returns:** [SiteLayout](/reference/graphql/objects/#sitelayout)
**Arguments**
* `previewID`: `String`
* `version`: [ContentVersion](/reference/graphql/enums/#contentversion)
## orderByPublicId
[Section titled “orderByPublicId”](#orderbypublicid)
```graphql
orderByPublicId(
publicId: ID!
): Order
```
Get order details for anonymous/public viewing using a public ID. Used for order confirmation pages accessible without authentication
Example:
```graphql
query {
orderByPublicId(publicId: "ae7fbbd3-9474-4be7-b321-2842a2294331") {
id
orderNumber
orderState
totalPrice {
value
currencyCode
}
lineItems {
id
quantity
productVariant {
sku
}
}
shippingAddress {
firstName
lastName
city
}
}
}
```
**Returns:** [Order](/reference/graphql/objects/#order)
**Arguments**
* `publicId`: `ID`!
## \_empty
[Section titled “\_empty”](#_empty)
```graphql
_empty: String
```
Placeholder query to satisfy schema requirements for federation
**Returns:** `String`
# Scalars
> GraphQL scalars exposed by the Evolve gateway.
10 scalars in the Evolve GraphQL schema.
## Boolean
[Section titled “Boolean”](#boolean)
The `Boolean` scalar type represents `true` or `false`.
*Custom scalar.*
## Currency
[Section titled “Currency”](#currency)
*Custom scalar.*
## Date
[Section titled “Date”](#date)
Represents a date.
*Custom scalar.*
## DateTime
[Section titled “DateTime”](#datetime)
Represents a date and time.
*Custom scalar.*
## EmailAddress
[Section titled “EmailAddress”](#emailaddress)
*Custom scalar.*
## Float
[Section titled “Float”](#float)
The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).
*Custom scalar.*
## ID
[Section titled “ID”](#id)
The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
*Custom scalar.*
## Int
[Section titled “Int”](#int)
The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
*Custom scalar.*
## String
[Section titled “String”](#string)
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
*Custom scalar.*
## Time
[Section titled “Time”](#time)
*Custom scalar.*
# Unions
> GraphQL unions exposed by the Evolve gateway.
44 unions in the Evolve GraphQL schema.
## Block
[Section titled “Block”](#block)
Represents different types of content blocks that can be used in pages.
**Possible types:** [CatalogBlock](/reference/graphql/objects/#catalogblock), [HeroBlock](/reference/graphql/objects/#heroblock), [OneColumnBlock](/reference/graphql/objects/#onecolumnblock), [ProductBlock](/reference/graphql/objects/#productblock), [TeaserBlock](/reference/graphql/objects/#teaserblock), [TeasersBlock](/reference/graphql/objects/#teasersblock), [ThreeColumnsBlock](/reference/graphql/objects/#threecolumnsblock), [TwoColumnsBlock](/reference/graphql/objects/#twocolumnsblock), [UspBlock](/reference/graphql/objects/#uspblock)
## BusinessUnitAddressCreateError
[Section titled “BusinessUnitAddressCreateError”](#businessunitaddresscreateerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## BusinessUnitAddressDeleteError
[Section titled “BusinessUnitAddressDeleteError”](#businessunitaddressdeleteerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## BusinessUnitAddressUpdateError
[Section titled “BusinessUnitAddressUpdateError”](#businessunitaddressupdateerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## BusinessUnitAssociateInviteError
[Section titled “BusinessUnitAssociateInviteError”](#businessunitassociateinviteerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## BusinessUnitAssociateRemoveError
[Section titled “BusinessUnitAssociateRemoveError”](#businessunitassociateremoveerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## BusinessUnitAssociateRolesUpdateError
[Section titled “BusinessUnitAssociateRolesUpdateError”](#businessunitassociaterolesupdateerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## CartDiscountCodeAddError
[Section titled “CartDiscountCodeAddError”](#cartdiscountcodeadderror)
Error union for cart discount code add
**Possible types:** [CartError](/reference/graphql/objects/#carterror), [ValidationError](/reference/graphql/objects/#validationerror)
## CartDiscountCodeRemoveError
[Section titled “CartDiscountCodeRemoveError”](#cartdiscountcoderemoveerror)
Error union for cart discount code remove
**Possible types:** [CartError](/reference/graphql/objects/#carterror), [ValidationError](/reference/graphql/objects/#validationerror)
## CartLineItemsAddError
[Section titled “CartLineItemsAddError”](#cartlineitemsadderror)
Error union for cart line items add
**Possible types:** [CartError](/reference/graphql/objects/#carterror), [ValidationError](/reference/graphql/objects/#validationerror)
## CartLineItemsRemoveError
[Section titled “CartLineItemsRemoveError”](#cartlineitemsremoveerror)
Error union for cart line items remove
**Possible types:** [CartError](/reference/graphql/objects/#carterror), [ValidationError](/reference/graphql/objects/#validationerror)
## CartLineItemsUpdateError
[Section titled “CartLineItemsUpdateError”](#cartlineitemsupdateerror)
Error union for cart line items update
**Possible types:** [CartError](/reference/graphql/objects/#carterror), [ValidationError](/reference/graphql/objects/#validationerror)
## CartUpdateError
[Section titled “CartUpdateError”](#cartupdateerror)
Error union for cart update
**Possible types:** [CartError](/reference/graphql/objects/#carterror), [ValidationError](/reference/graphql/objects/#validationerror)
## CheckoutCompleteErrorUnion
[Section titled “CheckoutCompleteErrorUnion”](#checkoutcompleteerrorunion)
**Possible types:** [CheckoutCompleteError](/reference/graphql/objects/#checkoutcompleteerror), [ValidationError](/reference/graphql/objects/#validationerror)
## ColumnEntry
[Section titled “ColumnEntry”](#columnentry)
Represents content types that can be used in columns.
**Possible types:** [Divider](/reference/graphql/objects/#divider), [Faq](/reference/graphql/objects/#faq), [Image](/reference/graphql/objects/#image), [PageTitle](/reference/graphql/objects/#pagetitle), [RichText](/reference/graphql/objects/#richtext), [YoutubeVideo](/reference/graphql/objects/#youtubevideo)
## CreatePaymentErrorUnion
[Section titled “CreatePaymentErrorUnion”](#createpaymenterrorunion)
**Possible types:** [PaymentCreateError](/reference/graphql/objects/#paymentcreateerror), [ValidationError](/reference/graphql/objects/#validationerror)
## CustomerAddressCreateError
[Section titled “CustomerAddressCreateError”](#customeraddresscreateerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## CustomerAddressDeleteError
[Section titled “CustomerAddressDeleteError”](#customeraddressdeleteerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## CustomerAddressUpdateError
[Section titled “CustomerAddressUpdateError”](#customeraddressupdateerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## CustomerCreateError
[Section titled “CustomerCreateError”](#customercreateerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## CustomerDeleteError
[Section titled “CustomerDeleteError”](#customerdeleteerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## CustomerEmailUpdateError
[Section titled “CustomerEmailUpdateError”](#customeremailupdateerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## CustomerInformationUpdateError
[Section titled “CustomerInformationUpdateError”](#customerinformationupdateerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## CustomerLoginError
[Section titled “CustomerLoginError”](#customerloginerror)
**Possible types:** [AuthenticationError](/reference/graphql/objects/#authenticationerror), [ValidationError](/reference/graphql/objects/#validationerror)
## CustomerPasswordResetError
[Section titled “CustomerPasswordResetError”](#customerpasswordreseterror)
**Possible types:** [PasswordResetError](/reference/graphql/objects/#passwordreseterror), [ValidationError](/reference/graphql/objects/#validationerror)
## CustomerPasswordResetRequestError
[Section titled “CustomerPasswordResetRequestError”](#customerpasswordresetrequesterror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## CustomerPasswordUpdateError
[Section titled “CustomerPasswordUpdateError”](#customerpasswordupdateerror)
**Possible types:** [PasswordChangeError](/reference/graphql/objects/#passwordchangeerror), [ValidationError](/reference/graphql/objects/#validationerror)
## Facet
[Section titled “Facet”](#facet)
Union of different facet types for product filtering
**Possible types:** [CategoryFacet](/reference/graphql/objects/#categoryfacet), [OptionsFacet](/reference/graphql/objects/#optionsfacet), [RangeFacet](/reference/graphql/objects/#rangefacet), [ToggleFacet](/reference/graphql/objects/#togglefacet)
## Media
[Section titled “Media”](#media)
Union of different media types available for products
**Possible types:** [ProductImage](/reference/graphql/objects/#productimage), [ProductImage360](/reference/graphql/objects/#productimage360), [ProductYouTubeVideo](/reference/graphql/objects/#productyoutubevideo)
## MenuLinkTarget
[Section titled “MenuLinkTarget”](#menulinktarget)
Represents either a ResourceLink or a ProductCategory.
**Possible types:** [ProductCategory](/reference/graphql/objects/#productcategory), [ResourceLink](/reference/graphql/objects/#resourcelink)
## NavigationContent
[Section titled “NavigationContent”](#navigationcontent)
Represents different types of content blocks that can be used in navigation menu.
**Possible types:** [RichText](/reference/graphql/objects/#richtext)
## Page
[Section titled “Page”](#page)
Represents either a ContentPage or a CatalogPage.
**Possible types:** [CatalogPage](/reference/graphql/objects/#catalogpage), [ContentPage](/reference/graphql/objects/#contentpage)
## PaymentMethodExtra
[Section titled “PaymentMethodExtra”](#paymentmethodextra)
**Possible types:** [ApplePayExtra](/reference/graphql/objects/#applepayextra), [OtherPaymentMethodExtra](/reference/graphql/objects/#otherpaymentmethodextra)
## ProductSpecificationAttribute
[Section titled “ProductSpecificationAttribute”](#productspecificationattribute)
**Possible types:** [ProductSpecificationBooleanAttribute](/reference/graphql/objects/#productspecificationbooleanattribute), [ProductSpecificationDateTimeAttribute](/reference/graphql/objects/#productspecificationdatetimeattribute), [ProductSpecificationEnumAttribute](/reference/graphql/objects/#productspecificationenumattribute), [ProductSpecificationEnumListAttribute](/reference/graphql/objects/#productspecificationenumlistattribute), [ProductSpecificationNumberAttribute](/reference/graphql/objects/#productspecificationnumberattribute), [ProductSpecificationStringAttribute](/reference/graphql/objects/#productspecificationstringattribute)
## QuoteRequestCreateError
[Section titled “QuoteRequestCreateError”](#quoterequestcreateerror)
Error union for quote request creation
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## RefreshTokenError
[Section titled “RefreshTokenError”](#refreshtokenerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## SetActiveBusinessContextError
[Section titled “SetActiveBusinessContextError”](#setactivebusinesscontexterror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## ShoppingListCreateError
[Section titled “ShoppingListCreateError”](#shoppinglistcreateerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## ShoppingListDeleteError
[Section titled “ShoppingListDeleteError”](#shoppinglistdeleteerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## ShoppingListLineItemAddError
[Section titled “ShoppingListLineItemAddError”](#shoppinglistlineitemadderror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## ShoppingListLineRemoveError
[Section titled “ShoppingListLineRemoveError”](#shoppinglistlineremoveerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## ShoppingListLineUpdateError
[Section titled “ShoppingListLineUpdateError”](#shoppinglistlineupdateerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## ShoppingListUpdateError
[Section titled “ShoppingListUpdateError”](#shoppinglistupdateerror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)
## ValidateApplePayPaymentError
[Section titled “ValidateApplePayPaymentError”](#validateapplepaypaymenterror)
**Possible types:** [ValidationError](/reference/graphql/objects/#validationerror)