diff --git a/core/broadleaf-framework-web/pom.xml b/core/broadleaf-framework-web/pom.xml
index c732740dce..0c7dcfa040 100644
--- a/core/broadleaf-framework-web/pom.xml
+++ b/core/broadleaf-framework-web/pom.xml
@@ -94,5 +94,13 @@
org.springframework.security
spring-security-oauth2-client
+
+ org.springframework.boot
+ spring-boot-starter-graphql
+
+
+ com.graphql-java
+ graphql-java-extended-scalars
+
diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLConfig.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLConfig.java
new file mode 100644
index 0000000000..7069b09085
--- /dev/null
+++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLConfig.java
@@ -0,0 +1,38 @@
+/*-
+ * #%L
+ * BroadleafCommerce Framework Web
+ * %%
+ * Copyright (C) 2009 - 2026 Broadleaf Commerce
+ * %%
+ * Licensed under the Broadleaf Fair Use License Agreement, Version 1.0
+ * (the "Fair Use License" located at http://license.broadleafcommerce.org/fair_use_license-1.0.txt)
+ * unless the restrictions on use therein are violated and require payment to Broadleaf in which case
+ * the Broadleaf End User License Agreement (EULA), Version 1.1
+ * (the "Commercial License" located at http://license.broadleafcommerce.org/commercial_license-1.1.txt)
+ * shall apply.
+ *
+ * Alternatively, the Commercial License may be replaced with a mutually agreed upon license (the "Custom License")
+ * between you and Broadleaf Commerce. You may not use this file except in compliance with the applicable license.
+ * #L%
+ */
+package org.broadleafcommerce.core.web.graphql;
+
+import graphql.scalars.ExtendedScalars;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.graphql.execution.RuntimeWiringConfigurer;
+
+/**
+ * Registers Broadleaf-specific GraphQL runtime wiring, including the
+ * {@code BigDecimal} scalar used for monetary amounts. The scalar preserves
+ * the full precision of {@link java.math.BigDecimal}; serializing via
+ * {@code GraphQLFloat} would silently degrade values to IEEE 754 doubles.
+ */
+@Configuration
+public class GraphQLConfig {
+
+ @Bean
+ public RuntimeWiringConfigurer runtimeWiringConfigurer() {
+ return wiringBuilder -> wiringBuilder.scalar(ExtendedScalars.GraphQLBigDecimal);
+ }
+}
diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java
new file mode 100644
index 0000000000..dc97093ed5
--- /dev/null
+++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLContextInterceptor.java
@@ -0,0 +1,185 @@
+/*-
+ * #%L
+ * BroadleafCommerce Framework Web
+ * %%
+ * Copyright (C) 2009 - 2026 Broadleaf Commerce
+ * %%
+ * Licensed under the Broadleaf Fair Use License Agreement, Version 1.0
+ * (the "Fair Use License" located at http://license.broadleafcommerce.org/fair_use_license-1.0.txt)
+ * unless the restrictions on use therein are violated and require payment to Broadleaf in which case
+ * the Broadleaf End User License Agreement (EULA), Version 1.1
+ * (the "Commercial License" located at http://license.broadleafcommerce.org/commercial_license-1.1.txt)
+ * shall apply.
+ *
+ * Alternatively, the Commercial License may be replaced with a mutually agreed upon license (the "Custom License")
+ * between you and Broadleaf Commerce. You may not use this file except in compliance with the applicable license.
+ * #L%
+ */
+package org.broadleafcommerce.core.web.graphql;
+
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.broadleafcommerce.common.util.StringUtil;
+import org.broadleafcommerce.common.web.BroadleafRequestContext;
+import org.broadleafcommerce.core.order.domain.Order;
+import org.broadleafcommerce.core.order.service.OrderService;
+import org.broadleafcommerce.core.web.order.CartState;
+import org.broadleafcommerce.profile.core.domain.Customer;
+import org.broadleafcommerce.profile.core.service.CustomerService;
+import org.broadleafcommerce.profile.web.core.CustomerState;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.graphql.server.WebGraphQlInterceptor;
+import org.springframework.graphql.server.WebGraphQlRequest;
+import org.springframework.graphql.server.WebGraphQlResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.context.request.ServletWebRequest;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import jakarta.servlet.http.HttpServletRequest;
+import reactor.core.publisher.Mono;
+
+/**
+ * A {@link WebGraphQlInterceptor} that establishes the Broadleaf customer and cart
+ * context for GraphQL requests. The customer identity is resolved following the same
+ * precedence used by {@code RestApiCustomerStateFilter}: request attribute first, then
+ * request parameter, then request header. If no customer can be resolved, an anonymous
+ * customer is created. Once the customer is established, the active cart is populated
+ * into {@link CartState} for downstream resolvers.
+ */
+@Component
+public class GraphQLContextInterceptor implements WebGraphQlInterceptor {
+
+ public static final String CUSTOMER_ID_ATTRIBUTE = "customerId";
+ public static final String BLC_RULE_MAP_PARAM = "blRuleMap";
+
+ protected static final Log LOG = LogFactory.getLog(GraphQLContextInterceptor.class);
+
+ protected final CustomerService customerService;
+ protected final OrderService orderService;
+
+ @Autowired
+ public GraphQLContextInterceptor(
+ @Qualifier("blCustomerService") CustomerService customerService,
+ @Qualifier("blOrderService") OrderService orderService
+ ) {
+ this.customerService = customerService;
+ this.orderService = orderService;
+ }
+
+ @Override
+ public Mono intercept(WebGraphQlRequest request, Chain chain) {
+ HttpServletRequest httpRequest = extractHttpRequest(request);
+ if (httpRequest != null) {
+ populateCustomerAndCart(httpRequest);
+ }
+ return chain.next(request);
+ }
+
+ protected HttpServletRequest extractHttpRequest(WebGraphQlRequest request) {
+ RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+ if (requestAttributes instanceof ServletRequestAttributes servletRequestAttributes) {
+ return servletRequestAttributes.getRequest();
+ }
+ return null;
+ }
+
+ protected void populateCustomerAndCart(HttpServletRequest request) {
+ Customer customer = resolveCustomer(request);
+ if (customer == null) {
+ customer = customerService.createCustomer();
+ CustomerState.setCustomer(customer);
+ setupCustomerForRuleProcessing(customer, request);
+ }
+ populateCart(customer);
+ }
+
+ protected Customer resolveCustomer(HttpServletRequest request) {
+ String customerId = readCustomerId(request);
+
+ if (customerId != null && customerId.trim().length() > 0) {
+ Long parsedId = parseCustomerId(customerId);
+ if (parsedId != null) {
+ Customer customer = customerService.readCustomerById(parsedId);
+ if (customer != null) {
+ ensureWebRequest(request);
+ CustomerState.setCustomer(customer);
+ setupCustomerForRuleProcessing(customer, request);
+ return customer;
+ }
+ } else {
+ LOG.warn(String.format("The customer id passed in '%s' was not a valid long", StringUtil.sanitize(customerId)));
+ }
+ } else if (LOG.isDebugEnabled()) {
+ LOG.debug("No customer ID was found for the GraphQL request. In order to look up a customer for the request"
+ + " send a request parameter or request header for the '" + CUSTOMER_ID_ATTRIBUTE + "' attribute");
+ }
+
+ Customer existingCustomer = CustomerState.getCustomer();
+ if (existingCustomer != null) {
+ return existingCustomer;
+ }
+
+ ensureWebRequest(request);
+ return null;
+ }
+
+ protected Long parseCustomerId(String customerId) {
+ if (!NumberUtils.isDigits(customerId)) {
+ return null;
+ }
+ try {
+ return Long.valueOf(customerId);
+ } catch (NumberFormatException ex) {
+ return null;
+ }
+ }
+
+ protected String readCustomerId(HttpServletRequest request) {
+ String customerId = null;
+ if (request.getAttribute(CUSTOMER_ID_ATTRIBUTE) != null) {
+ customerId = String.valueOf(request.getAttribute(CUSTOMER_ID_ATTRIBUTE));
+ }
+ if (customerId == null) {
+ customerId = request.getParameter(CUSTOMER_ID_ATTRIBUTE);
+ }
+ if (customerId == null) {
+ customerId = request.getHeader(CUSTOMER_ID_ATTRIBUTE);
+ }
+ return customerId;
+ }
+
+ protected void ensureWebRequest(HttpServletRequest request) {
+ BroadleafRequestContext ctx = BroadleafRequestContext.getBroadleafRequestContext();
+ if (ctx != null && ctx.getWebRequest() == null) {
+ ctx.setWebRequest(new ServletWebRequest(request));
+ }
+ }
+
+ protected void populateCart(Customer customer) {
+ if (customer == null) {
+ return;
+ }
+ Order cart = orderService.findCartForCustomer(customer);
+ if (cart != null) {
+ CartState.setCart(cart);
+ }
+ }
+
+ protected void setupCustomerForRuleProcessing(Customer customer, HttpServletRequest request) {
+ @SuppressWarnings("unchecked")
+ Map ruleMap = (Map) request.getAttribute(BLC_RULE_MAP_PARAM);
+ if (ruleMap == null) {
+ ruleMap = new HashMap<>();
+ }
+ ruleMap.put("customer", customer);
+ request.setAttribute(BLC_RULE_MAP_PARAM, ruleMap);
+ }
+
+}
diff --git a/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLExceptionResolver.java b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLExceptionResolver.java
new file mode 100644
index 0000000000..d313216f55
--- /dev/null
+++ b/core/broadleaf-framework-web/src/main/java/org/broadleafcommerce/core/web/graphql/GraphQLExceptionResolver.java
@@ -0,0 +1,92 @@
+/*-
+ * #%L
+ * BroadleafCommerce Framework Web
+ * %%
+ * Copyright (C) 2009 - 2026 Broadleaf Commerce
+ * %%
+ * Licensed under the Broadleaf Fair Use License Agreement, Version 1.0
+ * (the "Fair Use License" located at http://license.broadleafcommerce.org/fair_use_license-1.0.txt)
+ * unless the restrictions on use therein are violated and require payment to Broadleaf in which case
+ * the Broadleaf End User License Agreement (EULA), Version 1.1
+ * (the "Commercial License" located at http://license.broadleafcommerce.org/commercial_license-1.1.txt)
+ * shall apply.
+ *
+ * Alternatively, the Commercial License may be replaced with a mutually agreed upon license (the "Custom License")
+ * between you and Broadleaf Commerce. You may not use this file except in compliance with the applicable license.
+ * #L%
+ */
+package org.broadleafcommerce.core.web.graphql;
+
+import graphql.ErrorClassification;
+import graphql.ErrorType;
+import graphql.GraphQLError;
+import graphql.GraphqlErrorBuilder;
+import graphql.schema.DataFetchingEnvironment;
+import org.broadleafcommerce.core.checkout.service.exception.CheckoutException;
+import org.broadleafcommerce.core.offer.service.exception.OfferMaxUseExceededException;
+import org.broadleafcommerce.core.order.service.exception.AddToCartException;
+import org.broadleafcommerce.core.order.service.exception.IllegalCartOperationException;
+import org.broadleafcommerce.core.order.service.exception.RemoveFromCartException;
+import org.broadleafcommerce.core.order.service.exception.UpdateCartException;
+import org.broadleafcommerce.core.pricing.service.exception.PricingException;
+import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * Maps Broadleaf domain exceptions to structured GraphQL errors with stable
+ * {@code code} extensions so clients can branch on a known error taxonomy.
+ */
+@Component
+public class GraphQLExceptionResolver extends DataFetcherExceptionResolverAdapter {
+
+ public static final String CODE_ADD_TO_CART = "ADD_TO_CART_ERROR";
+ public static final String CODE_PRICING = "PRICING_ERROR";
+ public static final String CODE_OFFER_MAX_USE_EXCEEDED = "OFFER_MAX_USE_EXCEEDED";
+ public static final String CODE_ILLEGAL_CART_OPERATION = "ILLEGAL_CART_OPERATION";
+ public static final String CODE_CHECKOUT = "CHECKOUT_ERROR";
+ public static final String CODE_REMOVE_FROM_CART = "REMOVE_FROM_CART_ERROR";
+ public static final String CODE_UPDATE_CART = "UPDATE_CART_ERROR";
+
+ @Override
+ protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
+ if (ex instanceof AddToCartException) {
+ return buildError(ex, env, ErrorType.ValidationError, CODE_ADD_TO_CART);
+ }
+ if (ex instanceof RemoveFromCartException) {
+ return buildError(ex, env, ErrorType.ValidationError, CODE_REMOVE_FROM_CART);
+ }
+ if (ex instanceof UpdateCartException) {
+ return buildError(ex, env, ErrorType.ValidationError, CODE_UPDATE_CART);
+ }
+ if (ex instanceof IllegalCartOperationException) {
+ return buildError(ex, env, ErrorType.ValidationError, CODE_ILLEGAL_CART_OPERATION);
+ }
+ if (ex instanceof OfferMaxUseExceededException) {
+ return buildError(ex, env, ErrorType.ValidationError, CODE_OFFER_MAX_USE_EXCEEDED);
+ }
+ if (ex instanceof CheckoutException) {
+ return buildError(ex, env, ErrorType.ValidationError, CODE_CHECKOUT);
+ }
+ if (ex instanceof PricingException) {
+ return buildError(ex, env, ErrorType.DataFetchingException, CODE_PRICING);
+ }
+ return null;
+ }
+
+ protected GraphQLError buildError(
+ Throwable ex,
+ DataFetchingEnvironment env,
+ ErrorClassification classification,
+ String code
+ ) {
+ String message = ex.getMessage() != null ? ex.getMessage() : ex.getClass().getSimpleName();
+ return GraphqlErrorBuilder.newError(env)
+ .message(message)
+ .errorType(classification)
+ .extensions(Map.of("code", code))
+ .build();
+ }
+
+}
diff --git a/core/broadleaf-framework-web/src/main/resources/config/bc/web/common.properties b/core/broadleaf-framework-web/src/main/resources/config/bc/web/common.properties
index f98baafc07..4770c1d49c 100644
--- a/core/broadleaf-framework-web/src/main/resources/config/bc/web/common.properties
+++ b/core/broadleaf-framework-web/src/main/resources/config/bc/web/common.properties
@@ -40,3 +40,10 @@ googleAnalytics4.tagIdForProperty=
# This approach can be more efficient for large catalogs and more easily support dynamic URL building
allowProductResolutionUsingIdParam=false
allowCategoryResolutionUsingIdParam=false
+
+# GraphQL endpoint configuration. Schema files are loaded from classpath:graphql/
+# and GraphiQL is enabled by default; override in a consuming application's
+# common.properties to disable for production environments.
+spring.graphql.graphiql.enabled=true
+spring.graphql.path=/graphql
+spring.graphql.schema.locations=classpath:graphql/
diff --git a/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls b/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls
new file mode 100644
index 0000000000..a7b0e6b4e1
--- /dev/null
+++ b/core/broadleaf-framework-web/src/main/resources/graphql/schema.graphqls
@@ -0,0 +1,258 @@
+type Query {
+ # Catalog
+ product(id: ID!): Product
+ productByExternalId(externalId: String!): Product
+ productByUri(uri: String!): Product
+ products(name: String, limit: Int = 25, offset: Int = 0): [Product!]!
+ category(id: ID!): Category
+ categoryByUri(uri: String!): Category
+ categories(name: String, limit: Int = 25, offset: Int = 0): [Category!]!
+ sku(id: ID!): Sku
+ skuByUpc(upc: String!): Sku
+ search(query: String!, limit: Int = 25, offset: Int = 0): [Product!]!
+
+ # Cart & Orders
+ cart: Order
+ order(id: ID!): Order
+ orderByNumber(orderNumber: String!): Order
+ orderHistory(status: OrderStatus): [Order!]!
+
+ # Account
+ customer: Customer
+ customerAddresses: [CustomerAddress!]!
+}
+
+type Mutation {
+ # Cart
+ addToCart(input: AddToCartInput!): Order!
+ updateCartItemQuantity(orderItemId: ID!, quantity: Int!): Order!
+ removeFromCart(orderItemId: ID!): Order!
+ applyPromoCode(code: String!): PromoCodeResult!
+ removePromoCode(offerCodeId: ID!): Order!
+
+ # Checkout
+ saveShippingInfo(input: ShippingInfoInput!): Order!
+ saveBillingInfo(input: BillingInfoInput!): Order!
+ saveOrderEmail(email: String!): Order!
+ processCheckout(orderId: ID!): CheckoutResult!
+
+ # Account
+ updateAccount(input: UpdateAccountInput!): Customer!
+ changePassword(input: ChangePasswordInput!): Boolean!
+ addCustomerAddress(input: CustomerAddressInput!): CustomerAddress!
+ updateCustomerAddress(input: CustomerAddressInput!): CustomerAddress!
+ removeCustomerAddress(id: ID!): Boolean!
+}
+
+type Product {
+ id: ID!
+ name: String
+ description: String
+ longDescription: String
+ url: String
+ manufacturer: String
+ defaultCategory: Category
+ defaultSku: Sku
+ additionalSkus: [Sku!]!
+ productOptions: [ProductOption!]!
+}
+
+type Category {
+ id: ID!
+ name: String
+ description: String
+ longDescription: String
+ url: String
+ activeSubCategories(limit: Int = 25, offset: Int = 0): [Category!]!
+ products(limit: Int = 25, offset: Int = 0): [Product!]!
+}
+
+type Sku {
+ id: ID!
+ name: String
+ description: String
+ salePrice: Money
+ retailPrice: Money
+ upc: String
+ quantityAvailable: Int
+ available: Boolean
+}
+
+type Money {
+ amount: BigDecimal
+ currency: String
+}
+
+type Order {
+ id: ID!
+ orderNumber: String
+ status: OrderStatus
+ subTotal: Money
+ total: Money
+ totalTax: Money
+ totalShipping: Money
+ orderItems: [OrderItem!]!
+ fulfillmentGroups: [FulfillmentGroup!]!
+ payments: [OrderPayment!]!
+ emailAddress: String
+ customer: Customer
+ addedOfferCodes: [OfferCode!]!
+ itemCount: Int
+}
+
+type OrderItem {
+ id: ID!
+ name: String
+ quantity: Int
+ price: Money
+ salePrice: Money
+ retailPrice: Money
+ totalPrice: Money
+ product: Product
+ sku: Sku
+}
+
+type FulfillmentGroup {
+ id: ID!
+ address: Address
+ fulfillmentOption: FulfillmentOption
+ fulfillmentPrice: Money
+ items: [FulfillmentGroupItem!]!
+}
+
+type FulfillmentGroupItem {
+ id: ID!
+ orderItem: OrderItem
+ quantity: Int
+}
+
+type FulfillmentOption {
+ id: ID!
+ name: String
+ description: String
+ price: Money
+}
+
+type OrderPayment {
+ id: ID!
+ type: String
+ amount: Money
+ billingAddress: Address
+}
+
+type OfferCode {
+ id: ID!
+ offerCode: String
+}
+
+type Customer {
+ id: ID!
+ firstName: String
+ lastName: String
+ emailAddress: String
+ username: String
+}
+
+type CustomerAddress {
+ id: ID!
+ addressName: String
+ address: Address
+}
+
+type Address {
+ id: ID!
+ addressLine1: String
+ addressLine2: String
+ city: String
+ stateProvinceRegion: String
+ postalCode: String
+ country: String
+ phonePrimary: String
+}
+
+type ProductOption {
+ id: ID!
+ label: String
+ type: String
+ required: Boolean
+ values: [ProductOptionValue!]!
+}
+
+type ProductOptionValue {
+ id: ID!
+ attributeValue: String
+ displayOrder: Int
+ priceAdjustment: Money
+}
+
+type PromoCodeResult {
+ order: Order!
+ promoAdded: Boolean!
+ exception: String
+}
+
+type CheckoutResult {
+ order: Order!
+ success: Boolean!
+ errorMessage: String
+}
+
+enum OrderStatus {
+ IN_PROCESS
+ SUBMITTED
+ CANCELLED
+}
+
+scalar BigDecimal
+
+input AddToCartInput {
+ productId: ID!
+ skuId: ID
+ quantity: Int!
+ itemAttributes: [ItemAttributeInput!]
+}
+
+input ItemAttributeInput {
+ name: String!
+ value: String!
+}
+
+input ShippingInfoInput {
+ address: AddressInput!
+ fulfillmentOptionId: ID!
+ personalMessage: String
+ deliveryMessage: String
+}
+
+input BillingInfoInput {
+ address: AddressInput!
+ paymentType: String
+}
+
+input AddressInput {
+ addressLine1: String!
+ addressLine2: String
+ city: String!
+ stateProvinceRegion: String!
+ postalCode: String!
+ country: String!
+ phonePrimary: String
+}
+
+input UpdateAccountInput {
+ firstName: String
+ lastName: String
+ emailAddress: String
+}
+
+input ChangePasswordInput {
+ currentPassword: String!
+ newPassword: String!
+ newPasswordConfirm: String!
+}
+
+input CustomerAddressInput {
+ id: ID
+ addressName: String!
+ address: AddressInput!
+}
diff --git a/pom.xml b/pom.xml
index 6836d3b269..dd28511854 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,6 +47,8 @@
6.5.9
3.3.6
3.5.13
+ 1.3.4
+ 22.0
2.21.2
1.18.44
1.5.32
@@ -1202,6 +1204,28 @@
spring-boot-autoconfigure
${spring.boot.version}
+
+ org.springframework.boot
+ spring-boot-starter-graphql
+ ${spring.boot.version}
+
+
+ com.graphql-java
+ graphql-java-extended-scalars
+ ${graphql-java-extended-scalars.version}
+
+
+ com.graphql-java
+ graphql-java
+
+
+
+
+
+ org.reactivestreams
+ reactive-streams
+ 1.0.4
+
org.springframework.security
spring-security-oauth2-client