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/). [![Evolve data model](/img/evolve-data-model.png)](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