Server helpers and proxy
Import the server helpers from @avvyr/storefront-next/server. That entry point imports server-only, so if client code imports it by mistake the build fails before the private key can leak.
// lib/storefront.ts
import { createStorefrontServer } from '@avvyr/storefront-next/server'
import config from '@/storefront.config'
export const storefront = createStorefrontServer(config)
Every helper reads the current request through next/headers, so pages that use them render dynamically, like a server-rendered Nuxt page. The helpers that touch the network are cached per request with React cache(), so a layout and a page that both call storefront.session() share a single API call.
Helpers
| Helper | Returns |
|---|---|
storefront.client(options?) | SDK client for the request's market, with the private key. Options: market (another market), privateKey: false, fetch (for example, to add Next cache options). |
storefront.customerClient() | Client acting as the signed-in customer, or null without a session cookie |
storefront.session() | { accessToken, customer } after verifying the session with the platform, or null |
storefront.requireSession() | Like session(), but redirects to auth.loginPath?returnTo=… when there is no session |
storefront.cart() | The visitor's cart from the cookies, or null |
storefront.cartClient() | { client, cartId } with the private key and the visitor's cart token, for server-only cart calls |
storefront.market() | The active MarketConfig |
storefront.marketInfo() | The market's settings from the API (delivery countries, pricesRequireLogin), or null |
storefront.categories({ locale? }) | Categories from the in-memory, stale-while-revalidate cache |
storefront.initialState({ cart?, customer? }) | The props for <StorefrontProvider> |
storefront.warmCategories() | Warms the category cache for every market (call it from instrumentation.ts) |
Server Components
// app/products/[handle]/page.tsx
import { notFound } from 'next/navigation'
import { storefront } from '@/lib/storefront'
export default async function ProductPage({ params }: { params: Promise<{ handle: string }> }) {
const { handle } = await params
const client = await storefront.client()
const product = await client.products.getByHandle(handle).catch(() => null)
if (!product) notFound()
return <ProductView product={product} />
}
Protected pages
// app/account/page.tsx
export default async function AccountPage() {
const { customer } = await storefront.requireSession()
const orders = await (await storefront.customerClient())!.orders.list()
return <Account customer={customer} orders={orders} />
}
auth.protectedPaths in the config redirects signed-out visitors in the proxy before anything renders. It checks only that a session cookie exists, as the Nuxt auth middleware does. requireSession() also verifies the session with the platform.
Server Actions
cart.complete and other calls that need the private key are rejected from a browser (403 SERVER_SIDE_KEY_REQUIRED). Call them from a Server Action or a Route Handler:
'use server'
import { storefront } from '@/lib/storefront'
export async function completeOrder(payment: { amount: number; currency: string }) {
const ctx = await storefront.cartClient()
if (!ctx) throw new Error('No cart')
return ctx.client.cart.complete(ctx.cartId, { payment })
}
The proxy
createStorefrontProxy(config) from @avvyr/storefront-next/proxy runs before every matched request:
- Market. It resolves the market with the configured strategy and passes it to the server in the
x-avvyr-marketrequest header. With thepathstrategy it also rewrites the prefix away (/no/productsrendersapp/products/page.tsxfor the marketno). - Session. If the access token has expired, or expires within
auth.refreshSkewSeconds, the proxy refreshes it and writes the new tokens to both the response and the request that the page renders with. Server Components cannot set cookies, and the platform ends a session when a rotated refresh token is used again, so this is the only safe place to refresh on the server. Concurrent requests that carry the same refresh token share one refresh. If no refresh token is still valid, the proxy deletes the session cookies in every scope where they may have been set. - Protected paths. Visitors without a session who request a path under
auth.protectedPathsare redirected to the login page withreturnTo.
To run your own logic first (for example, a password gate for staging), wrap the proxy:
const storefrontProxy = createStorefrontProxy(storefrontConfig)
export async function proxy(request: NextRequest) {
if (needsPassword(request)) return NextResponse.redirect(new URL('/gate', request.url))
return storefrontProxy(request)
}
Without the proxy, the domain and cookie strategies still work and the browser still refreshes tokens on its own. However, the path strategy is not available, and a Server Component render after the access token expires sees the visitor as signed out until the browser has refreshed.
Head scripts
<StorefrontScripts config market /> renders the dataLayer and Google Tag Manager bootstrap first in <head>. It sets up the dataLayer, sets Consent Mode v2 to "denied", restores a choice already stored in the consent cookie, and only then loads the GTM container. It renders nothing when there is no GTM id and Consent Mode is off. NEXT_PUBLIC_GTM_ID takes precedence over config.analytics.gtm.id, so you can set it in production only.