Agent Skill · Spree Commerce

spree-storefront

Use when the user is working on the optional Next.js storefront (the customer-facing online store) — adding a page, customizing checkout, fetching products, integrating with Spree's Store API. Common phrasings include "customize storefront", "Next.js storefront", "frontend changes", "PDP", "product page", "cart", "checkout flow", "@spree/sdk", "publishable key". Provides the storefront architecture, the @spree/sdk integration model, and the storefront-vs-backend decision tree.

Provider: Spree Commerce Path in repo: skills/spree-storefront/SKILL.md

Skill body

Spree Storefront (Next.js)

The Spree Next.js storefront is a separate application that talks to the Spree backend over the v3 Store API. It’s hosted at github.com/spree/storefront and cloned separately into your project tree.

The storefront is optional. Headless deployments may use a custom frontend — React Native, Astro, Remix, or hand-rolled. Spree’s job is to expose a clean API; what consumes it is your choice. This skill assumes the official Next.js storefront, but the API contract is identical for any frontend.

How it connects to Spree

Browser ──HTTPS──> Next.js storefront ──API──> Spree backend (Rails)
                          │
                          └── @spree/sdk for typed API calls

The storefront authenticates against the Spree backend via a publishable API key (pk_… prefix). Customer-bound operations (their cart, their account) use additional auth — JWT for logged-in customers, cart tokens for guest carts.

# .env.local — server-side only (the storefront makes all API calls via Server Actions)
SPREE_API_URL=http://localhost:3000
SPREE_PUBLISHABLE_KEY=pk_…

@spree/sdk — the canonical client

Don’t hand-write fetch calls. Use @spree/sdk for typed access to the Store API:

import { createClient } from '@spree/sdk'

const spree = createClient({
  baseUrl: process.env.SPREE_API_URL!,
  publishableKey: process.env.SPREE_PUBLISHABLE_KEY!,
})

// List products
const { data, meta } = await spree.products.list({
  expand: ['media', 'default_variant'],
})

// Get a single product by slug or prefixed ID
const product = await spree.products.get('cool-shirt')

// Create a cart
const cart = await spree.carts.create()

// Add item to cart — cart ID positional, token via options.spreeToken
await spree.carts.items.create(cart.id, {
  variant_id: 'variant_k5nR8xLq',
  quantity: 1,
}, { spreeToken: cart.token })

The SDK includes:

Authentication modes

Who How Use for
Anonymous browser Publishable key Browsing products, viewing categories
Guest cart Publishable key + cart token Cart operations for not-yet-signed-up customers
Logged-in customer Publishable key + JWT (customer login) Order history, saved addresses, account pages

The customer login flow:

const { token, refresh_token, user } = await spree.auth.login({
  email: 'jane@example.com',
  password: 'secret',
})

// Pass the JWT per request via options.token
const orders = await spree.customer.orders.list({}, { token })
const me = await spree.customer.get({ token })

// Refresh later
const { token: newToken } = await spree.auth.refresh({ refresh_token })

Channels — which sales surface

If the merchant has multiple channels (website, mobile app, in-store POS), the storefront should identify which one it represents. Set the channel via the SDK config:

const spree = createClient({
  baseUrl: process.env.SPREE_API_URL!,
  publishableKey: process.env.SPREE_PUBLISHABLE_KEY!,
  channel: 'online',         // channel code; or prefixed ID like 'ch_…'
})

The Spree backend uses Spree::Current.channel to scope queries — only products published on that channel surface in API responses. If channel is omitted, the store’s default channel is used.

Common storefront patterns

Server-rendered PDP

// app/products/[slug]/page.tsx
import { spree } from '@/lib/spree'

export default async function ProductPage({ params }: { params: { slug: string } }) {
  const product = await spree.products.get(params.slug, {
    expand: ['default_variant', 'variants', 'media', 'categories'],
  })

  return (
    <main>
      <h1>{product.name}</h1>
      <img src={product.media?.[0]?.large_url ?? undefined} alt={product.media?.[0]?.alt ?? ''} />
      <AddToCartButton variantId={product.default_variant_id} />
    </main>
  )
}

The v3 Store API uses flat responsesproduct.name, not product.data.attributes.name. Related records appear as either ID fields (e.g. default_variant_id) or, when expanded, as nested objects (e.g. product.default_variant, product.media[]).

Client-side cart

Carts are server-state, so use SWR or React Query. The cart ID + token persist in a cookie:

'use client'
import useSWR from 'swr'

export function MiniCart({ cartId, token }: { cartId: string; token: string }) {
  const { data: cart } = useSWR(
    ['cart', cartId],
    () => spree.carts.get(cartId, { spreeToken: token })
  )
  if (!cart) return null
  return <span>{cart.items.length} items</span>
}

Checkout

The Store API exposes payment sessions for the checkout flow — a single, provider-agnostic endpoint that works with any session-based gateway (Stripe, Adyen, PayPal); the provider is selected via payment_method_id. The pattern:

  1. Customer hits checkout — POST /api/v3/store/carts/:cart_id/payment_sessions with a payment method choice (cart token in X-Spree-Token header).
  2. Backend returns a session with provider-specific data (Stripe Checkout URL, Adyen drop-in token, etc.).
  3. Storefront redirects to the provider OR renders the provider’s embedded form.
  4. Customer completes — provider posts back to the Spree backend, which fires payment_session.completed events.
  5. Storefront calls spree.carts.paymentSessions.complete(cartId, sessionId, { session_result: 'success' }, options) once the customer confirms, then spree.carts.complete(cartId, options) to get the Order — or relies on the provider webhook, in which case the backend completes the cart → order transition automatically.

The spree_stripe / spree_adyen / spree_paypal_checkout gems ship reference checkout flows. Don’t roll your own unless you’re integrating a new provider.

Webhook handling

For Next.js storefronts, @spree/sdk/webhooks provides HMAC signature verification with typed event payloads:

// app/api/webhooks/spree/route.ts
import { verifyWebhookSignature, type WebhookEvent } from '@spree/sdk/webhooks'

export async function POST(req: Request) {
  const body = await req.text()
  const signature = req.headers.get('x-spree-webhook-signature') ?? ''
  const timestamp = req.headers.get('x-spree-webhook-timestamp') ?? ''

  if (!verifyWebhookSignature(body, signature, timestamp, process.env.SPREE_WEBHOOK_SECRET!)) {
    return new Response('Invalid signature', { status: 401 })
  }

  const event: WebhookEvent = JSON.parse(body)
  switch (event.name) {
    case 'order.completed':
      await sendCustomThankYouEmail(event.data)
      break
    case 'order.shipped':
      await pushShippingNotification(event.data)
      break
  }

  return Response.json({ received: true })
}

The Spree backend ships outbound webhooks as Spree::WebhookEndpoint records. Configure URL + events under Settings → Webhooks in the admin.

Storefront vs backend — where does the change belong

Want to… Belongs in
Change how a product is displayed (layout, colors, copy) Storefront
Add a new field to product responses Backend (model + serializer)
Add a custom page like /about, /shipping Storefront
Change pricing logic Backend (service swap or extension)
Add a country to checkout Backend (Markets / Country config)
Customize the checkout UI flow Storefront
Add an A/B test to the PDP Storefront
Sync orders to a CRM Backend (subscriber)
Custom analytics events Storefront (client-side tracking) OR backend (subscriber) — depends on what triggers them
Customize the cart total calculation Backend (service swap on Spree.cart_recalculate_service)
Send a custom transactional email Backend (subscriber + ActionMailer)

The rule: anything customer-visible is the storefront. Anything that touches data, money, or business logic is the backend. When in doubt, backend — keeping logic centralized makes it consistent across all frontends if you ever ship a second one.

Common gotchas

Where to read further

Work with this as data

Every skill here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for agent skills

4 MCP tools reach this
  • find_skillsBrowse and filter every skill in the catalog.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools

Call it yourself

curl for this page
This skill
curl "https://apis.io/api/v1/skills/spree-storefront"
All agent skills
curl "https://apis.io/api/v1/skills?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no email required.

A second provider on the same verified email joins the account you already have.