@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, anduseAuthare 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 key | Env var | Notes |
|---|---|---|
shopwareStorefrontUrl | NEXT_PUBLIC_SHOPWARE_STOREFRONT_URL | Base host of the store, e.g. https://your-store.shopware.cloud. |
shopwareAccessKey | NEXT_PUBLIC_SHOPWARE_ACCESS_KEY | The sales-channel sw-access-key. Sent on every Store API call. |
Optional:
| Config key | Notes |
|---|---|
shopwareCurrencyCode | ISO currency code used to label prices (Shopware returns amounts only). EUR. |
shopwareLanguageId | Sent as sw-language-id when set. |
shopwareCurrencyId | Sent as sw-currency-id when set. |
shopwareRootCategoryId | Category used by useFacet to derive global aggregations. |
shopwareRecoveryStorefrontUrl | Origin the password-recovery link is built from. See Authentication. |
setAuthCookie / cookieName | Whether 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.
createAPIClientis configured withaccessToken(sent as the sales-channelsw-access-key) and an initialcontextTokenseeded from theContextTokenStore(see below).- Hooks call the SDK's fully-typed
invoke('<operationId> <method> /path', { pathParams, body }), which returns{ data, status }and throwsApiClientErroron non-2xx responses. - The rotated
sw-context-tokenis captured through the SDK'sonContextChangedhook 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-genis 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-contextcookie, 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'scookies()) when constructing the client.
Store setup required for browser-side calls. Shopware returns the rotated token only as an
sw-context-tokenresponse header — never in the body. A browser can only read that header cross-origin if the Shopware host sendsAccess-Control-Expose-Headers: sw-context-token. Without it the header is hidden from JS,onContextChangednever fires, and every browser session silently stays anonymous — carts and logins included. Server-side calls (SSR, next-authauthorize, 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.
| Method | Endpoint |
|---|---|
login | POST /store-api/account/login, then POST /store-api/account/customer |
logout | POST /store-api/account/logout |
exists / getCustomer | GET /api/middleware/shopware/exists → Admin API POST /api/search/customer |
password.passwordResetEmail | POST /store-api/account/recovery-password |
password.passwordReset | POST /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
- Store API reference: https://shopware.stoplight.io/docs/store-api
- Request headers (
sw-access-key,sw-context-token,sw-language-id,sw-currency-id): https://developer.shopware.com/docs/guides/integrations-api/general-concepts/request-headers.html