Loading...
Loading...
Use when the user is working on the optional Next.js storefront for Spree 6 (the customer-facing store in `apps/storefront/`, github.com/spree/storefront) — adding a page, customizing checkout, fetching products, multi-region URLs, the wholesale/B2B portal, transactional emails from webhooks, storefront tests, or integrating any frontend with the Store API via `@spree/sdk`. Common phrasings include "customize storefront", "Next.js storefront", "frontend changes", "PDP", "product page", "cart", "checkout flow", "order confirmation page", "@spree/sdk", "publishable key", "country/locale URLs", "wholesale portal", "storefront emails", "webhook route". Provides the storefront architecture, the @spree/sdk integration model, and the storefront-vs-backend decision tree.
npx skill4agent add spree/agent-skills spree-storefront/api/v3/store/*@spree/sdkcreate-spree-appapps/storefront/server/apps/dashboard/Browser → Server Action (src/lib/data/*) → @spree/sdk → Spree Store API
httpOnly cookies (JWT, refresh token, cart token, country/locale) via src/lib/spree helperssrc/lib/data/SPREE_PUBLISHABLE_KEYNEXT_PUBLIC_src/lib/spree/getClient()withAuthRefresh()getLocaleOptions()getCartOptions()CartContextCheckoutContext// src/lib/data/customer.ts (pattern)
import { getClient, withAuthRefresh, setAccessToken, setRefreshToken } from '@/lib/spree'
export async function login(email: string, password: string) {
const { token, refresh_token, user } = await getClient().auth.login({ email, password })
await setAccessToken(token)
await setRefreshToken(refresh_token)
return { success: true, user }
}
export async function getCustomer() {
return withAuthRefresh((options) => getClient().customer.get(options))
}.env.localSPREE_API_URLSPREE_PUBLISHABLE_KEYNEXT_PUBLIC_DEFAULT_COUNTRYNEXT_PUBLIC_DEFAULT_LOCALENEXT_PUBLIC_SITE_URLSPREE_WHOLESALE_CHANNELSPREE_WHOLESALE_PUBLISHABLE_KEYSPREE_WEBHOOK_SECRETRESEND_API_KEYEMAIL_FROMNEXT_PUBLIC_*import { createClient } from '@spree/sdk'
const spree = createClient({
baseUrl: process.env.SPREE_API_URL!,
publishableKey: process.env.SPREE_PUBLISHABLE_KEY!,
locale: 'en', // sent as x-spree-locale (also per request: { locale })
channel: 'online', // sent as X-Spree-Channel; omit for the store default
})
const { data, meta } = await spree.products.list({ expand: ['media', 'default_variant'] })
const product = await spree.products.get('cool-shirt', { expand: ['variants', 'media'] }) // slug or prod_ id
const cart = await spree.carts.create()
await spree.carts.items.create(cart.id, { variant_id: 'variant_k5nR8xLq', quantity: 1 }, { spreeToken: cart.token })
const orders = await spree.customer.orders.list({}, { token }) // logged-in customer (JWT)product.name*_idexpandproduct.default_variantproduct.primary_mediaproduct.media[]Priceproduct.price.display_amountspreeTokenx-spree-tokentoken@spree/sdk@spree/sdk/zod@spree/sdk/webhooksprod_variant_cart_or_cust_ful_cart.idcart.numberidcart_…or_…const cart = await spree.carts.get(cartId, { spreeToken })
cart.requirements
// [{ step: 'address', field: 'ship_address', code: 'ship_address_required', message: '…' },
// { step: 'delivery', field: 'delivery_method', code: 'delivery_method_required', message: '…' }, …]requirements[]{ step, field, code, message }line_items_requiredemail_requiredship_address_requireddelivery_method_requiredpayment_requiredpo_number_requiredguest_checkout_not_allowedquantity_rule_violatedorder_minimum_not_metdiscontinuedout_of_stockcartaddressdeliverypaymentmessageSpree::Checkout::Registry.add_requirementspree-checkoutspree.carts.fulfillments.update(cartId, fulfillmentId, { selected_delivery_rate_id }, opts)spree.carts.paymentSessions.create(cartId, { payment_method_id }, opts)spree.carts.paymentSessions.complete(cartId, sessionId, { session_result }, opts)spree.carts.complete(cartId, opts)Order | OrderGroupogrp_…isOrderGroup@spree/sdkspree.carts.payments.create(...)carts.getspree.orders.get(id, params, { spreeToken | token })or_…cart_…order-placed/[id]statusdraftplacedcanceledpayment_statusfulfillment_statussrc/lib/data/checkout.tspayment.tscompleteCheckoutOrdercomponents/checkout/*spree_stripesrc/app/[country]/[locale]/…/us/en/products/de/de/productssrc/proxy.tscreateSpreeMiddleware({ defaultCountry, defaultLocale, staticRoutes? })@/lib/spree/middleware/{country}/{locale}/…spree_countryx-vercel-ip-countrycf-ipcountryspree_localeAccept-Languagespree_countryspree_localegetLocaleOptions()src/app/[country]/[locale]/(storefront)/your-page/page.tsxCountrySwitcher/wholesaleSPREE_WHOLESALE_CHANNELchannel: 'wholesale'X-Spree-Channelstorefront_accesslogin_requiredprices_hiddenpublicnullspree-b2bsrc/lib/emails/.next/emails//dev/emailssrc/app/api/webhooks/spree/route.tscreateWebhookHandler@/lib/spree/webhooksX-Spree-Webhook-Signature"{timestamp}.{body}"import { createWebhookHandler } from '@/lib/spree/webhooks'
const handler = createWebhookHandler({
secret: process.env.SPREE_WEBHOOK_SECRET!,
handlers: {
'order.placed': handleOrderPlaced,
'order.canceled': handleOrderCanceled,
'order.fulfilled': handleOrderFulfilled, // or 'fulfillment.fulfilled' for per-parcel emails
'customer.password_reset_requested': handlePasswordReset, // must be subscribed by name — `*` never receives it
},
// waitUntil, toleranceSeconds optional
})
export const POST = handlerorder.placedorder.completedorder.fulfilledfulfillment.fulfilledorder.shippedorder.completedorder.shippedorder.placednotify_customer: falseorder_group.completedspree-marketplacesend_consumer_transactional_emailsnotify_customer: falsenew_order_notifications_emailSPREE_WEBHOOK_SECRETcloudflared tunnel --url http://localhost:3001idevent.idwaitUntil{ handled: false }verifyWebhookSignature(body, signature, timestamp, secret)type WebhookEvent<T>@spree/sdk/webhooksspree-events-webhookspnpm testpnpm test:watchsrc/lib/data/__tests__STRIPE_PUBLISHABLE_KEYSTRIPE_SECRET_KEYpnpm e2e:upghcr.io/spree/spree:latest@spree/cli.env.e2epnpm test:e2e:uipnpm e2e:downSPREE_IMAGE=<image> pnpm e2e:upcreate-spree-appdocker build . -f server/Dockerfile -t project-spree:e2e| Want to… | Belongs in |
|---|---|
| Layout, copy, styling, a new content page | Storefront ( |
| Add a field to product/cart responses | Backend — model + serializer swap ( |
| Change pricing | Backend — price lists / rules ( |
| Change how cart totals are calculated | Backend — |
| Add a checkout requirement (e.g. "accept terms") | Backend |
| Add a country / currency / language | Backend Markets; storefront just follows the URL + cookies |
| Customize the checkout UI | Storefront ( |
| A/B test the PDP, client analytics | Storefront ( |
| Sync orders to a CRM/ERP | Backend subscriber on |
| Customer email look & feel | Storefront emails (webhooks) or backend mailers — pick one owner |
sk_…updateTag(cartTag(surface))requirements[]carts.completecart_cannot_completerequirementscarts.getorders.getx-spree-localeX-Spree-ChannelAccept-Languageupstreamnode_modules/@spree/docs/dist/developer/storefront/nextjs/{architecture,customization,multi-region,wholesale,emails,environment-variables,testing,deployment,wallet-payments}.mddeveloper/sdk/quickstart.mddeveloper/tutorial/storefront.mdsrc/lib/spree/src/lib/data/src/app/api/webhooks/spree/route.tsspree-typescript-sdkspree-api-v3spree-checkoutspree-events-webhooksspree-b2bspree-i18nspree-deployment