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
├── components/
│ ├── cart/ # CartDrawer
│ ├── checkout/ # AddressStep, DeliveryStep, PaymentStep, etc.
│ ├── layout/ # Header, Footer, CountrySwitcher
│ ├── navigation/ # Breadcrumbs
│ ├── products/ # ProductCard, ProductGrid, Filters, MediaGallery, VariantPicker
│ └── search/ # SearchBar
├── contexts/
│ ├── AuthContext.tsx # Auth state
│ ├── CartContext.tsx # Client-side cart state sync
│ ├── CheckoutContext.tsx # Checkout flow state
│ └── 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
│ └── utils.ts # Shared helpers (actionResult, withFallback)
└── 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
@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 supports multiple countries and currencies via URL segments:
/us/en/products # US store, English
/de/de/products # German store, German
/uk/en/products # UK store, English
A middleware (src/proxy.ts) uses createSpreeMiddleware from src/lib/spree to detect the visitor's country and locale, then redirects to the correct URL prefix. The CountrySwitcher component lets users change regions manually.
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', ... })