docs/developer/storefront/nextjs/architecture.mdx
The storefront follows a server-first architecture where all API calls are made server-side. The Spree API key is never exposed to the browser.
Browser → Server Action → @spree/sdk → Spree API
(with httpOnly cookies via src/lib/spree helpers)
src/lib/data/) — call @spree/sdk directly with auth/cookie helpers from src/lib/spreegetLocaleOptions()src/
├── app/
│ └── [country]/[locale]/ # Localized routes
│ ├── (storefront)/ # Main storefront layout
│ │ ├── page.tsx # Homepage
│ │ ├── account/ # Customer account
│ │ │ ├── addresses/ # Address management
│ │ │ ├── credit-cards/ # Saved payment methods
│ │ │ ├── gift-cards/ # Gift cards
│ │ │ ├── orders/ # Order history
│ │ │ │ └── [id]/ # Order details
│ │ │ ├── profile/ # Profile settings
│ │ │ └── register/ # Registration
│ │ ├── cart/ # Shopping cart
│ │ ├── products/ # Product listing
│ │ │ └── [slug]/ # Product details
│ │ ├── t/[...permalink]/ # Category pages
│ │ └── categories/ # Category overview
│ ├── (checkout)/ # Checkout layout (no header/footer)
│ │ ├── checkout/[id]/ # Checkout flow
│ │ └── order-placed/[id]/ # Order confirmation
│ └── (wholesale)/ # Opt-in B2B portal (gated — see Wholesale guide)
│ └── wholesale/
│ ├── page.tsx # Trade catalog
│ ├── products/[slug]/ # Wholesale PDP
│ ├── cart/ # Wholesale cart
│ ├── quick-order/ # SKU quick-order form
│ ├── apply/ # Trade-account application
│ └── _components/ # Gate, header, sign-in wall, pending, guest browse
├── components/
│ ├── cart/ # CartDrawer
│ ├── checkout/ # AddressStep, DeliveryStep, PaymentStep, etc.
│ ├── layout/ # Header, Footer, CountrySwitcher
│ ├── navigation/ # Breadcrumbs
│ ├── products/ # ProductCard, ProductGrid, Filters, MediaGallery, HiddenPricePrompt
│ └── search/ # SearchBar
├── contexts/
│ ├── AuthContext.tsx # Auth state
│ ├── CartContext.tsx # Client-side cart state sync
│ ├── CheckoutContext.tsx # Checkout flow state
│ ├── HiddenPricingContext.tsx # Prices-hidden signal for wholesale guests
│ └── StoreContext.tsx # Store/locale/currency state
├── hooks/
│ ├── useCarouselProducts.ts # Product carousel data
│ └── useProductListing.ts # Product listing with filters
└── lib/
├── analytics/ # GTM integration
├── constants.ts # App constants
├── data/ # Server Actions (call @spree/sdk directly)
│ ├── addresses.ts # Address CRUD
│ ├── cart.ts # Cart operations
│ ├── checkout.ts # Checkout flow
│ ├── cookies.ts # Auth check helper
│ ├── countries.ts # Countries/regions
│ ├── credit-cards.ts # Payment methods
│ ├── customer.ts # Auth & profile
│ ├── gift-cards.ts # Gift cards
│ ├── orders.ts # Order history
│ ├── payment.ts # Payment processing
│ ├── products.ts # Product queries
│ ├── categories.ts # Categories
│ ├── wholesale.ts # Wholesale channel + catalog + quick-order
│ ├── utils.ts # Shared helpers (actionResult, withFallback)
│ └── … # One file per domain — see src/lib/data/
├── spree/ # Spree integration (client, cookies, auth refresh, middleware, surface)
├── wholesale.ts # Approval check + volume-pricing helpers
└── utils/ # Client utilities
├── address.ts # Address formatting
├── cookies.ts # Cookie helpers
├── credit-card.ts # Card formatting
├── path.ts # URL path helpers
└── product-query.ts # Product filter query builder
The (wholesale) route group is an opt-in B2B portal, off unless a wholesale channel is configured. Its gating, surfaces, and pricing modes are covered in the Wholesale Portal guide.
@spree/sdk to authenticatesrc/lib/spree cookie helperswithAuthRefresh() which reads the token from cookies automatically// src/lib/data/customer.ts
import { getClient, withAuthRefresh, setAccessToken, setRefreshToken } from '@/lib/spree'
export async function login(email: string, password: string) {
const result = await getClient().auth.login({ email, password })
await setAccessToken(result.token)
await setRefreshToken(result.refresh_token)
return { success: true, user: result.user }
}
export async function getCustomer() {
return withAuthRefresh(async (options) => {
return getClient().customer.get(options) // reads token from cookie
})
}
The storefront serves multiple countries, currencies, and languages from one deployment via /{country}/{locale} URL segments and an edge middleware that detects and persists the visitor's region. See the Multi-Region guide.
All data fetching is done through server actions in src/lib/data/. These call @spree/sdk directly, using src/lib/spree helpers for auth and locale:
// Products — use getLocaleOptions() for locale-aware reads
import { getProducts, getProduct, getProductFilters } from '@/lib/data/products'
const products = await getProducts({ limit: 12 })
const product = await getProduct('product-slug')
const filters = await getProductFilters()
// Cart — use getCartOptions()/requireCartId() for cart operations
import { getCart, addToCart, updateCartItem, removeCartItem } from '@/lib/data/cart'
const cart = await getCart()
await addToCart('var_xxx', 1)
await updateCartItem('li_xxx', 2)
await removeCartItem('li_xxx')
// Authentication — use withAuthRefresh() for authenticated endpoints
import { login, register, logout, getCustomer } from '@/lib/data/customer'
await login('[email protected]', 'password')
const customer = await getCustomer()
await logout()
// Addresses — use withAuthRefresh() for customer data
import { getAddresses, createAddress, updateAddress, deleteAddress } from '@/lib/data/addresses'
const addresses = await getAddresses()
await createAddress({ first_name: 'John', ... })