Cart Architecture
Carts are persisted on the server and referenced by a cookie:// Cart storage
const cartId = cookies().get('_openfront_cart_id')?.value
// Retrieve cart from backend
const cart = await openfrontClient.request(
gql`query GetCart($cartId: ID!) {
activeCart(cartId: $cartId)
}`,
{ cartId }
)
Cart Creation
Carts are automatically created when a customer adds their first item:// features/storefront/lib/data/cart.ts
export async function getOrSetCart(countryCode: string) {
let cartId = await getCartId()
let cart = null
// Try to retrieve existing cart
if (cartId) {
try {
const result = await openfrontClient.request(CART_QUERY, { cartId })
cart = result?.activeCart
// If cart doesn't exist, remove invalid cookie
if (!cart) {
await removeCartId()
cartId = undefined
}
} catch (error) {
await removeCartId()
cartId = undefined
}
}
// Get region for country
const { regions } = await openfrontClient.request(
gql`query GetRegion($code: String!) {
regions(where: { countries: { some: { iso2: { equals: $code } } } }) {
id
}
}`,
{ code: countryCode }
)
// Create new cart if needed
if (!cart) {
const { createCart: newCart } = await openfrontClient.request(
gql`mutation CreateCart($data: CartCreateInput!) {
createCart(data: $data) {
id
region { id }
}
}`,
{ data: { region: { connect: { id: regions[0].id } } } }
)
cart = newCart
await setCartId(cart.id)
}
return cart
}
Adding Items
Customers add products to cart by selecting a variant:// features/storefront/lib/data/cart.ts
export async function addToCart({
variantId,
quantity,
countryCode
}) {
if (!variantId) {
throw new Error('Missing variant ID when adding to cart')
}
// Get or create cart
const cart = await getOrSetCart(countryCode)
if (!cart) {
throw new Error('Error retrieving or creating cart')
}
// Add line item to cart
await openfrontClient.request(
gql`mutation UpdateActiveCart($cartId: ID!, $data: CartUpdateInput!) {
updateActiveCart(cartId: $cartId, data: $data) {
id
lineItems {
id
quantity
}
}
}`,
{
cartId: cart.id,
data: {
lineItems: {
create: [{
productVariant: { connect: { id: variantId } },
quantity
}]
}
}
}
)
revalidateTag('cart')
}
Product Actions Component
The add to cart button on product pages:// features/storefront/modules/products/components/product-actions/
export default function ProductActions({ product, region }) {
const [isAdding, setIsAdding] = useState(false)
const params = useParams()
const countryCode = params?.countryCode as string
const handleAddToCart = async () => {
if (!variant?.id) return null
setIsAdding(true)
await addToCart({
variantId: variant.id,
quantity: 1,
countryCode
})
setIsAdding(false)
}
return (
<Button
onClick={handleAddToCart}
disabled={isAdding || !inStock || !variant}
>
{isAdding && <Loader className="animate-spin" />}
{!variant ? 'Select variant' : !inStock ? 'Out of stock' : 'Add to cart'}
</Button>
)
}
Cart Page
The cart page (/[countryCode]/cart) displays all items and totals:
// features/storefront/modules/cart/templates/index.tsx
const CartTemplate = ({ cart, user }) => {
return (
<div className="py-12">
<div className="max-w-[1440px] mx-auto px-6">
{cart?.lineItems?.length ? (
<div className="grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-x-12">
{/* Left: Cart items */}
<div className="flex flex-col gap-y-6">
{!user && (
<>
<SignInPrompt />
<Divider />
</>
)}
<ItemsTemplate items={cart.lineItems} region={cart.region} />
</div>
{/* Right: Summary */}
<div className="sticky top-12">
<Summary cart={cart} />
</div>
</div>
) : (
<EmptyCartMessage />
)}
</div>
</div>
)
}
Line Items
Each cart item (line item) displays product details and quantity controls:// features/storefront/modules/cart/components/item/
const CartItem = ({ item, region }) => {
const { productVariant, quantity } = item
return (
<div className="flex gap-4">
{/* Product image */}
<div className="w-24 h-24 relative">
<Image
src={productVariant.product.thumbnail}
alt={productVariant.product.title}
fill
className="object-cover"
/>
</div>
{/* Product details */}
<div className="flex-1">
<h3>{productVariant.product.title}</h3>
<p className="text-muted-foreground">
{productVariant.title}
</p>
{/* Quantity selector */}
<QuantitySelect
quantity={quantity}
onUpdate={(newQuantity) =>
updateLineItem({ lineId: item.id, quantity: newQuantity })
}
/>
</div>
{/* Price */}
<div className="text-right">
<p className="font-semibold">
{formatAmount(item.total, region.currency.code)}
</p>
<button onClick={() => deleteLineItem(item.id)}>
Remove
</button>
</div>
</div>
)
}
Cart Operations
Update Line Item Quantity
export async function updateLineItem({ lineId, quantity }) {
const cartId = cookies().get('_openfront_cart_id')?.value
if (!cartId) return 'No cartId cookie found'
await openfrontClient.request(
gql`mutation UpdateLineItem(
$cartId: ID!,
$lineId: ID!,
$quantity: Int!
) {
updateActiveCartLineItem(
cartId: $cartId
lineId: $lineId
quantity: $quantity
) {
id
lineItems {
id
quantity
total
}
}
}`,
{ cartId, lineId, quantity }
)
revalidateTag('cart')
}
Remove Line Item
export async function deleteLineItem(lineId: string) {
const cartId = cookies().get('_openfront_cart_id')?.value
if (!cartId) return 'No cart ID found'
await openfrontClient.request(
gql`mutation UpdateActiveCart($cartId: ID!, $data: CartUpdateInput!) {
updateActiveCart(cartId: $cartId, data: $data) {
id
lineItems { id }
}
}`,
{
cartId,
data: {
lineItems: {
disconnect: [{ id: lineId }]
}
}
}
)
revalidateTag('cart')
}
Cart Summary
The cart summary displays totals and discount codes:const Summary = ({ cart }) => {
return (
<div className="bg-background p-6 border rounded-md">
<h2 className="text-2xl font-semibold mb-4">Summary</h2>
{/* Subtotal */}
<div className="flex justify-between mb-2">
<span>Subtotal</span>
<span>{formatAmount(cart.subtotal, cart.region.currency.code)}</span>
</div>
{/* Shipping */}
<div className="flex justify-between mb-2">
<span>Shipping</span>
<span>{cart.shippingTotal > 0
? formatAmount(cart.shippingTotal, cart.region.currency.code)
: 'Calculated at checkout'
}</span>
</div>
{/* Tax */}
<div className="flex justify-between mb-4">
<span>Tax</span>
<span>{formatAmount(cart.taxTotal, cart.region.currency.code)}</span>
</div>
<Divider />
{/* Total */}
<div className="flex justify-between text-xl font-bold mt-4">
<span>Total</span>
<span>{formatAmount(cart.total, cart.region.currency.code)}</span>
</div>
{/* Checkout button */}
<Button
className="w-full mt-6"
onClick={() => router.push(`/${countryCode}/checkout`)}
>
Checkout
</Button>
</div>
)
}
Discount Codes
Customers can apply discount codes to their cart:export async function submitDiscountForm(prevState: any, formData: FormData) {
const cartId = cookies().get('_openfront_cart_id')?.value
const code = formData.get('code') as string
if (!code) return 'Code is required'
if (!cartId) return 'No cart found'
try {
await addDiscountToCart(cartId, code)
revalidateTag('cart')
return null
} catch (error: any) {
// Extract error message from GraphQL response
if (error?.response?.errors?.[0]?.extensions?.originalError?.message) {
return error.response.errors[0].extensions.originalError.message
}
return 'Failed to apply discount code'
}
}
Discount Display
const DiscountCode = ({ cart }) => {
const [state, formAction] = useFormState(submitDiscountForm, null)
return (
<form action={formAction}>
<div className="flex gap-2">
<Input
name="code"
placeholder="Discount code"
className="flex-1"
/>
<Button type="submit">Apply</Button>
</div>
{state && <p className="text-sm text-red-500 mt-2">{state}</p>}
{/* Applied discounts */}
{cart.discounts?.map(discount => (
<div key={discount.id} className="flex justify-between mt-2">
<span className="text-sm">{discount.code}</span>
<button onClick={() => removeDiscount(discount.code)}>
Remove
</button>
</div>
))}
</form>
)
}
Gift Cards
Gift cards can be applied to reduce the cart total:export async function submitGiftCard(code: string) {
const cartId = await getCartId()
if (!cartId) return 'No cartId cookie found'
await openfrontClient.request(
gql`mutation UpdateActiveCart($cartId: ID!, $data: CartUpdateInput!) {
updateActiveCart(cartId: $cartId, data: $data) {
id
giftCards {
id
code
balance
}
}
}`,
{
cartId,
data: {
giftCards: {
connect: [{ code }]
}
}
}
)
revalidateTag('cart')
}
Guest Checkout
Guest customers can shop without creating an account:const SignInPrompt = () => (
<div className="bg-muted/40 p-4 rounded-md">
<p className="text-sm font-medium mb-2">
Already have an account?
</p>
<p className="text-sm text-muted-foreground mb-4">
Sign in for a better experience
</p>
<Button variant="outline" onClick={() => router.push('/account')}>
Sign in
</Button>
</div>
)
Regional Changes
When customers change regions, the cart is updated:export async function updateRegion(countryCode: string, currentPath: string) {
const cartId = cookies().get('_openfront_cart_id')?.value
// Revalidate data
revalidateTag('regions')
revalidateTag('products')
// Update cart region if cart exists
if (cartId) {
const { regions } = await openfrontClient.request(
gql`query GetRegion($code: String!) {
regions(where: { countries: { some: { iso2: { equals: $code } } } }) {
id
}
}`,
{ code: countryCode }
)
await openfrontClient.request(
gql`mutation UpdateActiveCart($cartId: ID!, $data: CartUpdateInput!) {
updateActiveCart(cartId: $cartId, data: $data) {
id
}
}`,
{
cartId,
data: { region: { connect: { id: regions[0].id } } }
}
)
revalidateTag('cart')
}
redirect(`/${countryCode}${currentPath}`)
}
Empty Cart
When the cart is empty, show a helpful message:const EmptyCartMessage = () => (
<div className="text-center py-12">
<ShoppingCart className="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<h2 className="text-2xl font-semibold mb-2">Your cart is empty</h2>
<p className="text-muted-foreground mb-6">
Looks like you haven't added anything to your cart yet
</p>
<Button onClick={() => router.push(`/${countryCode}`)}>
Start shopping
</Button>
</div>
)
Next Steps
Checkout
Learn about the multi-step checkout process
Cart API
GraphQL mutations for cart management