Skip to main content

@acromedia/gesso-shopware

Shopware 6 commerce plugin for Gesso. It implements the @acromedia/gesso-commerce hook surface (useCatalog, useProduct, useProductSearch, useFacet, useCart, useCustomer, useCheckout, useOrders, useAuth) on top of Shopware's REST Store API (/store-api).

Scaffold status. useCatalog, useProduct, useProductSearch, useFacet, useCart, useCheckout, useOrders, and useAuth are implemented. useCustomer (profile CRUD, registration, address book) is still a typed stub and lands in a follow-up ticket.

Installation

pnpm add @acromedia/gesso-shopware

@acromedia/gesso-commerce is a peer dependency.

Configuration

The plugin reads its connection settings from gesso.config.json (generated from .env by the plugin CLI). Two values are required:

Config keyEnv varNotes
shopwareStorefrontUrlNEXT_PUBLIC_SHOPWARE_STOREFRONT_URLBase host of the store, e.g. https://your-store.shopware.cloud.
shopwareAccessKeyNEXT_PUBLIC_SHOPWARE_ACCESS_KEYThe sales-channel sw-access-key. Sent on every Store API call.

Optional:

Config keyNotes
shopwareCurrencyCodeISO currency code used to label prices (Shopware returns amounts only). EUR.
shopwareLanguageIdSent as sw-language-id when set.
shopwareCurrencyIdSent as sw-currency-id when set.
shopwareRootCategoryIdCategory used by useFacet to derive global aggregations.
shopwareRecoveryStorefrontUrlOrigin the password-recovery link is built from. See Authentication.
setAuthCookie / cookieNameWhether useAuth maintains the client-side signed-in flag, and under what cookie name.

The Store API access key is public by design (it scopes requests to a sales channel) and is therefore exposed to the browser via the NEXT_PUBLIC_ prefix, the same way Shopify's storefront token is handled.

Running the plugin CLI (gesso-shopware config, or the host app's gesso config) reads the NEXT_PUBLIC_SHOPWARE_* env vars and writes the corresponding shopwareStorefrontUrl / shopwareAccessKey keys into gesso.config.json — the file the Store API client actually reads at runtime. .env stays the single source of truth.

Getting the sw-access-key

In the Shopware Administration: Sales Channels → [your Headless channel] → API access → API access key.

Transport client

This plugin talks to the Store API through the official @shopware/api-client SDK, pinned to an exact version (it has a fast release cadence), wrapped in a thin StoreApiClient (src/rest.ts). This mirrors how gesso-shopify wraps the official @shopify/graphql-client.

  • createAPIClient is configured with accessToken (sent as the sales-channel sw-access-key) and an initial contextToken seeded from the ContextTokenStore (see below).
  • Hooks call the SDK's fully-typed invoke('<operationId> <method> /path', { pathParams, body }), which returns { data, status } and throws ApiClientError on non-2xx responses.
  • The rotated sw-context-token is captured through the SDK's onContextChanged hook and written back to the store.
  • Store API request/response types come from the bundled @shopware/api-client/store-api-types, so generating pinned types with @shopware/api-gen is optional and not run in this scaffold.

Context token

Stateful Store API endpoints (cart, customer, checkout) require an sw-context-token, which the Store API rotates on login/register. The SDK captures the token the server returns (via its onContextChanged hook) and replays it on the next request; where that token is stored is pluggable through a ContextTokenStore (src/Context):

  • Browser (default): the token lives in the gesso-sw-context cookie, mirroring how BigCommerce round-trips its cart cookie.
  • Server (default): a no-op store, so SSR/RSC reads are anonymous. A module-level token would leak one visitor's session into another's request, so server persistence is opt-in.
  • SSR with a session: inject a request-scoped store via config.contextTokenStore (e.g. backed by Next's cookies()) when constructing the client.

Store setup required for browser-side calls. Shopware returns the rotated token only as an sw-context-token response header — never in the body. A browser can only read that header cross-origin if the Shopware host sends Access-Control-Expose-Headers: sw-context-token. Without it the header is hidden from JS, onContextChanged never fires, and every browser session silently stays anonymous — carts and logins included. Server-side calls (SSR, next-auth authorize, the middleware) are unaffected, since CORS does not apply.

Logging in does not discard the guest cart: Shopware's SalesChannelContextRestorer restores the customer's context and merges the guest cart into it (CartMergedEvent), so the rotated token still addresses the shopper's items.

Authentication

useAuth implements the @acromedia/gesso-commerce auth surface on Shopware's public account routes. The sales-channel access key is all they need, so — unlike BigCommerce, whose login is proxied to hide a secret — login, logout, and password recovery talk to Shopware directly and work unchanged in the browser and on the server.

MethodEndpoint
loginPOST /store-api/account/login, then POST /store-api/account/customer
logoutPOST /store-api/account/logout
exists / getCustomerGET /api/middleware/shopware/exists → Admin API POST /api/search/customer
password.passwordResetEmailPOST /store-api/account/recovery-password
password.passwordResetPOST /store-api/account/recovery-password-confirm

The session is the context token. Login answers with a rotated sw-context-token in the response headers and an empty body, so the plugin reads the profile back on the same client (which already replays the new token) and returns the token on the customer as options.accessToken — the same slot BigCommerce puts its customer access token in. That is the field GessoAuthOptions copies onto the next-auth JWT, and @acromedia/gesso-shopware-middleware reads it back to scope the customer's Store API calls — which is what makes a login performed inside next-auth's authorize (server-side, where the default context-token store is a deliberate no-op) carry its session forward.

Logout drops the gesso-sw-context cookie rather than adopting the fresh anonymous token Shopware hands back, so the next visit opens a clean guest session.

exists needs the middleware. Looking a customer up by email requires Admin API scope, so it goes through /api/middleware/shopware/exists, which answers with the bare customer id or a 404 — never the customer record. This is the contract next-auth's GessoAdapter.getUserByEmail reads. Because exists also runs server-side during sign-in, set GESSO_LOCAL_URL so the middleware URL has an origin to resolve against.

Rate limits. Shopware rate-limits these routes by default, with a time-backoff policy that resets after 24 hours — login at 10/10s, 15/30s, 20/60s, and reset_password at 3/30s, 5/60s, 10/90s. The recovery limit is the tight one: a user retrying a "forgot password" form three times in half a minute will hit it. Exceeding a limit surfaces as a thrown error carrying Shopware's own message (including how long to wait), the same as any other platform error.

Password recovery. Shopware validates the storefrontUrl it builds the reset link from against the sales channel's configured domains. In a decoupled build that is the Next.js origin, not the Shopware host, so set shopwareRecoveryStorefrontUrl; the plugin otherwise falls back to window.location.origin and finally to shopwareStorefrontUrl. The confirmation step is keyed only by the hash from the emailed link — there is no customer id in the flow — so the shared passwordReset(id, resetToken, password) signature carries the hash in the resetToken position (as Shopify and BigCommerce carry theirs), falling back to id.

Development

pnpm --filter @acromedia/gesso-shopware type-check
pnpm --filter @acromedia/gesso-shopware lint
pnpm --filter @acromedia/gesso-shopware build
pnpm --filter @acromedia/gesso-shopware test # Vitest suite (Store API mocked with MSW)

The suite is Vitest + MSW. There is also an opt-in live check that hits GET /store-api/context against a real store — set LIVE_TESTS=true plus NEXT_PUBLIC_SHOPWARE_STOREFRONT_URL / NEXT_PUBLIC_SHOPWARE_ACCESS_KEY in your environment. It is skipped by default.

References