Skip to main content

Server-First Pattern

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)
  • Server Actions (src/lib/data/) — call @spree/sdk directly with auth/cookie helpers from src/lib/spree
  • httpOnly Cookies — auth tokens, cart tokens, and locale are stored securely
  • No Client-Side API Calls — the Spree API key stays on the server
  • Auto-Localization — locale and country are read from cookies via getLocaleOptions()

Project Structure

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

Authentication Flow

  1. User submits login form
  2. Server action calls @spree/sdk to authenticate
  3. JWT token is stored in an httpOnly cookie via src/lib/spree cookie helpers
  4. Subsequent requests use withAuthRefresh() which reads the token from cookies automatically
  5. Token is never accessible to client-side JavaScript
// 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
  })
}

Multi-Region Support

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.

Server Actions

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('user@example.com', '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', ... })