Overview
@avvyr/storefront-next is the Next.js counterpart of the Nuxt layer. It is built on @avvyr/storefront-sdk and gives an App Router project the same building blocks:
- React hooks:
useCart,useAuth,useMarket,useProductSearch,useCheckoutand more, with the same names and behaviour as the Nuxt composables. State is shared across components through one provider. - Server helpers:
storefront.client(),storefront.session(),storefront.cart(),storefront.categories()for Server Components, Route Handlers and Server Actions. The private key stays on the server. - Session proxy: refreshes the customer's access token before a page renders, resolves the market, and redirects signed-out visitors away from protected paths.
- Drop-in components:
<AvvyrCookieConsent />,<AvvyrAssistant />and<AvvyrPaymentWidget />, the same components the Nuxt layer ships. See Components. - Analytics: the GA4 dataLayer, Google Consent Mode v2 and Avvyr behavioural tracking, wired into the cart, auth and checkout hooks.
- Full B2B support: customer registration, login (magic link, code and password), company accounts, order history and saved lists.
Requirements
- Next.js 15.3 or later (Next.js 16 recommended), App Router
- React 19
- Node.js 20.9 or later
Installation
npm install @avvyr/storefront-next @avvyr/storefront-sdk
Setup
The package needs six small files. The starter has all of them already.
1. storefront.config.ts
The equivalent of appConfig.storefront in the Nuxt layer. Server code and client code both import it, so it must not contain secrets. Keys come from environment variables.
import { defineStorefrontConfig } from '@avvyr/storefront-next/config'
export default defineStorefrontConfig({
store: {
strategy: 'cookie', // 'path' | 'domain' | 'cookie'
default: 'se',
markets: [
{ id: 'se', marketId: '676140c4136218caaf97d79e', name: 'Sverige', locale: 'sv-SE', i18n: 'sv', currency: 'SEK' },
{ id: 'no', marketId: '676140c4136218caaf97d780', name: 'Norge', locale: 'nb-NO', i18n: 'nb', currency: 'NOK' },
],
},
auth: {
protectedPaths: ['/account'], // signed-out visitors go to auth.loginPath (default /login)
},
})
Every other key has a default: cookie names, tracking, analytics, consent, assistant, variants and interceptors. They behave the same as in the Nuxt layer.
2. lib/storefront.ts
import { createStorefrontServer } from '@avvyr/storefront-next/server'
import config from '@/storefront.config'
export const storefront = createStorefrontServer(config)
3. proxy.ts
import { createStorefrontProxy } from '@avvyr/storefront-next/proxy'
import storefrontConfig from './storefront.config'
export const proxy = createStorefrontProxy(storefrontConfig)
// Next.js requires a static literal here.
export const config = { matcher: ['/((?!_next/|favicon.ico|.*\\..*).*)'] }
On Next.js 15, name the file middleware.ts and the export middleware.
4. app/providers.tsx
The provider is rendered from your own client component, because the config can contain functions (interceptors) and functions cannot be passed from the server as props.
'use client'
import { StorefrontProvider, type StorefrontInitialState } from '@avvyr/storefront-next'
import config from '@/storefront.config'
export function Providers({ initialState, children }: { initialState: StorefrontInitialState; children: React.ReactNode }) {
return (
<StorefrontProvider config={config} initialState={initialState}>
{children}
</StorefrontProvider>
)
}
5. app/layout.tsx
import { StorefrontScripts } from '@avvyr/storefront-next/server'
import '@avvyr/storefront-next/styles.css'
import config from '@/storefront.config'
import { storefront } from '@/lib/storefront'
import { Providers } from './providers'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const initialState = await storefront.initialState()
return (
<html lang={initialState.market.i18n}>
<head>
<StorefrontScripts config={config} market={initialState.market} />
</head>
<body>
<Providers initialState={initialState}>{children}</Providers>
</body>
</html>
)
}
initialState() reads the request's cookies, resolves the market, and fetches the cart on the server. The first render therefore already shows the cart, as the Nuxt layer's cart-restore plugin does.
6. app/api/storefront/categories/route.ts
import { createCategoriesHandler } from '@avvyr/storefront-next/server'
import { storefront } from '@/lib/storefront'
export const GET = createCategoriesHandler(storefront)
useCategories() reads from this route. The route serves the server's in-memory category cache (stale-while-revalidate, 5 minutes by default), so client navigation never calls the Avvyr API for categories. To warm the cache on boot, add instrumentation.ts:
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { storefront } = await import('@/lib/storefront')
void storefront.warmCategories()
}
}
Environment variables
NEXT_PUBLIC_STOREFRONT_BASE_URL=https://api.avvyr.com
NEXT_PUBLIC_STOREFRONT_PUBLIC_KEY=sfp_your_public_key
STOREFRONT_PRIVATE_KEY=sfs_your_private_key # server only
NEXT_PUBLIC_STOREFRONT_CDN_URL=https://assets.avvyr.com
NEXT_PUBLIC_GTM_ID= # optional, wins over config.analytics.gtm.id
STOREFRONT_CATEGORIES_TTL_MS=300000 # optional
Only the server reads these variables. The public values reach the browser through initialState, and the private key never does.
Market strategies
| Strategy | How the market is resolved | Switching |
|---|---|---|
path | URL prefix: /no/products. The default market has no prefix. The proxy rewrites the prefix away, so pages need no [market] segment. | Links use useMarket().path('/products') |
domain | Hostname: domain, stagingDomain or devDomain on each market | Navigate to the other domain |
cookie | The avvyr_market cookie (store.cookieName). Works on a single domain, which makes it practical in development | useMarket().switchMarket('no') writes the cookie and reloads |
How sessions work
Customer and cart tokens are stored in cookies, so the browser and the server see the same session.
- In the browser the SDK client retries any
401once after refreshing the token. Concurrent refreshes share a single request. - On the server, Server Components cannot write cookies. The platform rotates the refresh token every time it is used, and it ends the session if an old token is used again. The proxy therefore refreshes an access token that has expired, or expires within
auth.refreshSkewSeconds(default 120). It does this before rendering, writes the new cookies to the response, and passes them to the page in the same request. Requests that arrive at the same time with the same refresh token (for example, parallel link prefetches) share one refresh. - Signing in or out reloads the page so that every cache is rebuilt for the new identity. This includes product cards fetched while anonymous on a market with
pricesRequireLogin. Turn it off with<StorefrontProvider reloadOnSessionChange={false}>only if you invalidate those caches yourself.