A minimal, production-ready storefront template for Saleor.
Clean as a blank page β built to ship with agents and humans.
Tip
Questions or issues? Check our Discord for help.
Ship faster, customize everything. Paper is a new releaseβexpect some rough edgesβbut every component is built with real-world e-commerce in mind. This is a foundation you can actually build on.
The checkout is where most storefronts fall apart or fall short. Paper's doesn't β and checkout v2 aligns it with the rest of the stack: App Router, Server Components, server actions, and the same BFF session as the storefront (no client-side urql or browser Saleor tokens).
Storefront cart Checkout surface
βββββββββββββ ββββββββββββββββ
src/lib/checkout.ts src/app/(checkout)/checkout/
cookie + mutations β CheckoutSessionLoader (RSC)
@paper/session-bridge CheckoutApp β steps + payment
buildCheckoutPath() /checkout/complete?order= (confirmation)
- Server-first cart β RSC loads checkout +
meon entry; client context is a cache of server truth (CheckoutDataProvider). - URL-driven steps β
?step=contact|shipping|paymentupdates shallowly (no full page refetch per click); browser Back walks the funnel. - Dedicated confirmation β
/checkout/complete?order=is separate from the active cart route. - Extensible payments β Registry (
INTEGRATED_GATEWAYS) with Stripe + Dummy; add gateways viacheckout-payment-gatewaysskill. - Shared BFF auth β Sign-in via
/api/auth/login; session resolved server-side (resolveSessionUserβ guest / authenticated / unavailable). - Multi-step, mobile-first β Focused forms, international address fields, composable step components.
Developer docs: start at skills/saleor-paper-storefront/rules/paper-surfaces.md, then checkout-management.md. Forks on the old urql checkout: migrations/atomic/2026-06-checkout-v2/.
One codebase, many markets. Browse URLs are /{locale}/{channel}/β¦ β e.g. /en/us/products/hoodie (English, US market, USD) and /fr/fr/products/hoodie (French, France, EUR) β with legacy /{channel}/β¦ paths redirecting automatically. Each locale gets its own cached catalog payload, translated product copy from Saleor, per-channel pricing and currency, and hreflang/canonical metadata.
- Region picker β header control switches locale and channel together (language + market + currency)
- Three string systems β Saleor catalog translations, merchant-editable storefront content (CMS), and code-owned UI via next-intl (
messages/{locale}.json) - Six built-in locales β
en,pl,de,fr,fi,nb(extend viaLOCALE_DEFINITIONSinsrc/config/locale.ts)
Storefront channels are explicit. Saleor may have many channels (B2B, wholesale, internal regions); Paper only exposes the slugs you configure via STOREFRONT_CHANNELS. Disallowed channel URLs return 404. For a single-channel store, set NEXT_PUBLIC_DEFAULT_CHANNEL onlyβthe footer channel selector is hidden automatically.
Developer docs: docs/international-storefront.md Β· ADRs 0001 / 0002 Β· skills ui-locale-routing / ui-i18n
The hard parts are solved. Adapt the look, keep the logic.
- Partial Prerendering (PPR) β Product name, attributes, and SEO stay in a static cached shell; variant gallery and add-to-cart stream in via Suspense when
searchParamschange. - Multi-attribute variant selection β Color + Size + Material? Handled. Complex variant matrices just work.
- Dynamic pricing β Sale prices, variant-specific pricing, channel pricingβall reactive.
- Image gallery β Next.js Image optimization, proper aspect ratios, keyboard navigation.
Not an afterthought. Focus management on step transitions, keyboard navigation everywhere, semantic HTML, proper ARIA labels. Everyone deserves to shop.
Built for front-end developers and AI agents. The codebase includes:
AGENTS.mdβ Architecture overview and quick reference for AI assistantsskills/saleor-paper-storefront/β 21 task-specific rules covering GraphQL, caching, i18n, variant selection, checkout v2, and more- saleor/agent-skills β Universal Saleor API patterns; external skills via
pnpm skills:bootstrap(skills-lock.json) - Consistent patterns β Predictable structure that AI tools can navigate and modify confidently
Whether you're pair-programming with Cursor, Claude, or Copilotβthe codebase is designed to help them help you.
- Next.js 16 with App Router and Server Components
- React 19 with the latest concurrent features
- TypeScript in strict modeβyour IDE will thank you
- Tailwind CSS with design tokens (OKLCH colors, CSS variables)
- GraphQL Codegen for type-safe Saleor API calls
| Feature | Description |
|---|---|
| Checkout (v2) | RSC + server actions, shallow step URLs, payment registry (Stripe/Dummy), /checkout/complete |
| Cart | Slide-over drawer with real-time updates, quantity editing |
| Product Pages | Multi-attribute variants, image gallery, sticky add-to-cart |
| Product Listings | Category & collection pages with PPR (cached hero + dynamic filters), pagination |
| International | /{locale}/{channel}/ routing, region picker, Saleor translations, next-intl UI, hreflang SEO |
| Storefront content | Merchant-editable copy layer (code or Saleor Models) β homepage, cart trust, checkout editorial |
| Navigation | Dynamic menus from Saleor, mobile hamburger, breadcrumbs |
| SEO | Per-locale metadata, JSON-LD, Open Graph images, hreflang alternates |
| Caching | Cache Components (PPR), named cacheLife tiers, per-locale catalog cache, webhooks |
| Saleor Cloud Paper app | Saleor Cloud only β Dashboard extension for cache invalidation webhooks and Preview in storefront |
| Customer Profile | Account dashboard, address book, order history, password change, account deletion |
| Authentication | Login, register, password reset, guest checkout |
| API Resilience | Automatic retries, rate limiting, timeoutsβhandles flaky connections gracefully |
Paper uses Cache Components (Next.js 16 β "use cache", cacheLife, cacheTag behind cacheComponents: true) for optimal performance: static shells load instantly while dynamic content streams in. Learn more in the directive docs or see skills/saleor-paper-storefront/rules/data-caching.md for project-specific patterns.
The display-cached, checkout-live model ensures fast browsing with accurate checkout:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DATA FRESHNESS β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Product Pages Cart / Checkout Payment β
β ββββββββββββββ ββββββββββββββ βββββββ β
β β
β βββββββββββββ βββββββββββββ βββββββββββββ β
β β CACHED ββββββββββΆβ LIVE βββββββββββΆβ LIVE β β
β β 5 min β Add β Always β Pay β Always β β
β βββββββββββββ to βββββββββββββ βββββββββββββ β
β Cart β
β Fast page loads Real-time prices Saleor validates β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Component | Freshness | Why |
|---|---|---|
| Product pages | Cached (catalog) |
Static shell + dynamic variant islands (PPR) |
| Category/Collection | Cached (catalog) |
Cached hero from params; filters/pagination stream in Suspense |
| Homepage featured | Cached (catalog) |
Sync page shell; product grid streams in nested Suspense |
| Navigation / footer | Cached (menus) |
Per-channel tags; per-locale menu payloads in cache keys |
| Storefront content | Cached (menus) |
Tag storefront-content:{channel}:{locale} |
| Cart drawer | Always live | Saleor API with cache: "no-cache" |
| Checkout | Always live | RSC entry + server actions (cache: "no-cache"), real-time totals |
cacheLife tiers (see src/lib/cache-life-profiles.ts):
| Profile | Fallback TTL | Used for |
|---|---|---|
catalog |
~5 min | Products, categories, collections, homepage |
menus |
~1 hr | Header nav, footer menu |
channels |
~1 day | Footer channel metadata |
Webhook revalidateTag(tag, profile) clears data immediately; TTL is the safety net when webhooks are missing.
Browse URLs are /{locale}/{channel}/β¦. Cached catalog fetches pass localeSlug β separate cache entry per language, same warm-path speed. Invalidation uses slug-scoped tags (product:{slug}) and revalidates every locale path via buildPathsForAllLocales(). GraphQL uses Saleor base language codes (PL, not PL_PL). See data-caching.md Β§ Locale & Caching.
Cached GraphQL lives in src/lib/catalog/, src/lib/menus/, and src/lib/channels/ β not in layout or page components. Pages are thin orchestrators with nested <Suspense> for dynamic islands.
PDP β params only in the static shell; gallery and variant selection read searchParams:
ProductPage (sync)
βββ ProductShell β getProductData(slug, channel, locale) "use cache"
βββ h1, attributes, JSON-LD, LCP preload
βββ Suspense β VariantGalleryDynamic (searchParams)
βββ Suspense β VariantSectionDynamic (searchParams)
PLP (category, collection, all products) β cached hero/metadata from params; filter/sort/pagination in a dynamic grid:
Page
βββ CategoryHero β getCategoryData "use cache"
βββ Suspense β CategoryProducts (searchParams, always fresh fetch)
Homepage β sync <section> shell; featured collection grid in nested Suspense.
Loading UX β route-level loading.tsx files (products, categories, collections) show skeletons during navigation. The main layout does not wrap {children} in Suspense fallback={null}.
Cache tags (see src/lib/cache-manifest.ts):
| Tag pattern | Invalidated when |
|---|---|
product:{slug} |
Product updated (all locales) |
category:{slug} |
Category updated (all locales) |
collection:{slug} |
Collection updated (all locales) |
page:{slug} |
CMS page updated (all locales) |
navigation:{channel} |
Main menu changed for channel |
footer-menu:{channel} |
Footer menu changed for channel |
storefront-content:{channel}:{locale} |
Storefront Models page updated |
channels |
Channel list metadata |
Featured homepage products use tag collection:featured-products (same catalog profile as collections).
Saleor Cloud (recommended): Install the Saleor Cloud Paper app from Dashboard β Extensions. Available on Saleor Cloud only for now. It registers revalidation webhooks for products, categories, collections, pages, menus, and promotions; discovers cache tags via /api/cache-info; and adds Preview in storefront on product pages in Dashboard.
Self-hosted / manual setup:
- Create webhooks in Saleor Dashboard β Configuration β Webhooks
- Point to
https://your-store.com/api/revalidate - Subscribe to product/category/collection/page events; for menus use
MENU_*/MENU_ITEM_*and include{ menu: { slug } }fornavbarandfootermenus - Set
SALEOR_WEBHOOK_SECRETenv var
Manual revalidation (requires REVALIDATE_SECRET):
# Single product (all locale cache entries for slug)
curl "https://your-store.com/api/revalidate?secret=xxx&tag=product:blue-hoodie"
# Single product path (one locale; tag still clears all locales)
curl "https://your-store.com/api/revalidate?secret=xxx&tag=product:blue-hoodie&path=/pl/default-channel/products/blue-hoodie"
# CMS page (tag only β invalidates getPageData across channels)
curl "https://your-store.com/api/revalidate?secret=xxx&tag=page:about-us"
# Navigation for one channel (tag or tag + channel query)
curl "https://your-store.com/api/revalidate?secret=xxx&tag=navigation:us"
curl "https://your-store.com/api/revalidate?secret=xxx&tag=navigation&channel=us"
# All tags for every storefront channel
curl "https://your-store.com/api/revalidate?secret=xxx&all=1"Without webhooks? TTL handles itβcached data expires per the catalog / menus / channels profiles above.
- Saleor is the source of truth:
checkoutLinesAddcalculates prices server-side - Cart always fetches fresh: Users see current prices before checkout
- Payment validates:
checkoutCompleteuses real-time data
π Deep dive: See
skills/saleor-paper-storefront/rules/data-caching.mdfor the full architecture, Cache Components (PPR), webhook setup, and debugging guide.
Note
New to Saleor? Check out saleor.io/start to learn how storefronts work underneath.
Option A: Free Saleor Cloud account (recommended)
Option B: Run locally with Docker
# Using Saleor CLI (recommended)
npm i -g @saleor/cli@latest
saleor storefront create --url https://{YOUR_INSTANCE}/graphql/
# Or manually
git clone https://github.com/saleor/storefront.git
cd storefront
cp .env.example .env
pnpm install
# Optional β wire agent skills for Cursor (see "For AI Agents" below)
pnpm skills:bootstrapEdit .env with your Saleor instance details:
NEXT_PUBLIC_SALEOR_API_URL=https://your-instance.saleor.cloud/graphql/
NEXT_PUBLIC_DEFAULT_CHANNEL=default-channel # Your Saleor channel slugMulti-channel (recommended β explicit allowlist):
STOREFRONT_CHANNELS=us,uk,eu
NEXT_PUBLIC_DEFAULT_CHANNEL=us
NEXT_PUBLIC_STOREFRONT_LOCALES=en,pl,de,fr,fi,nb # URL locale slugs
SALEOR_APP_TOKEN=... # Server-side only β footer currency selector metadataFinding your channel slug: In Saleor Dashboard β Configuration β Channels β copy the slug
Note:
SALEOR_APP_TOKENalone no longer auto-discovers every Saleor channel. SetSTOREFRONT_CHANNELSor opt in withSTOREFRONT_DISCOVER_CHANNELS=true(see Environment Variables).
pnpm devOpen localhost:3000. That's it.
pnpm dev # Start dev server
pnpm build # Production build
pnpm run generate # Regenerate GraphQL types (storefront)
pnpm run generate:checkout # Regenerate GraphQL types (checkout)src/
βββ app/ # Next.js App Router
β βββ (storefront)/[locale]/[channel]/ # Browse, cart, account
β βββ (checkout)/checkout/ # Checkout route (/checkout)
βββ messages/ # next-intl UI strings (per locale)
βββ session-bridge/ # @paper/session-bridge β storefront β checkout handoff
βββ checkout/ # Checkout UI, providers, payment registry (GraphQL via server actions)
βββ graphql/ # GraphQL queries
βββ gql/ # Generated types (don't edit)
βββ lib/ # Server utilities & cached data layer
β βββ catalog/ # getCategoryData, getCollectionData, getFeaturedProducts
β βββ menus/ # getNavbarMenuItems, getFooterMenuItems
β βββ channels/ # getCachedChannelsList
β βββ cache-manifest.ts # Tag registry + cacheLife mapping
β βββ cache-life-profiles.ts
βββ ui/components/ # UI components
β βββ account/ # Customer profile & address book
β βββ pdp/ # Product detail page
β βββ plp/ # Product listing page
β βββ cart/ # Cart drawer
β βββ ui/ # Primitives (Button, Badge, etc.)
βββ styles/brand.css # Design tokens
If you're working with AI coding assistants, point them to:
AGENTS.mdβ Architecture, commands, gotchasskills/saleor-paper-storefront/β 21 project-specific rules (GraphQL, caching, i18n, checkout, etc.)skills/saleor-paper-storefront/references/code-conventions.mdβ File naming, exports, imports- saleor/agent-skills β Universal Saleor patterns and optional community skills
After clone, wire skills for Cursor discovery (repo-root skills/ is not scanned automatically):
pnpm skills:bootstrapSymlinks the project skill into .agents/skills/, then runs npx skills experimental_install from skills-lock.json. Do not run npx skills add . --skill saleor-paper-storefront β it copies a drifting snapshot.
# Required
NEXT_PUBLIC_SALEOR_API_URL=https://your-instance.saleor.cloud/graphql/
NEXT_PUBLIC_DEFAULT_CHANNEL=default-channel # Fallback channel; root "/" redirects here
# Multi-channel (recommended)
STOREFRONT_CHANNELS=us,uk,eu # Comma-separated allowlist β routes, revalidation, footer
# Optional
NEXT_PUBLIC_STOREFRONT_URL= # Canonical URLs and OG images
NEXT_PUBLIC_DEFAULT_LOCALE=en # Default URL locale slug
NEXT_PUBLIC_STOREFRONT_LOCALES=en,pl,de,fr,fi,nb # Enabled locale slugs
REVALIDATE_SECRET= # Manual cache invalidation (GET /api/revalidate)
SALEOR_WEBHOOK_SECRET= # Webhook HMAC verification
SALEOR_APP_TOKEN= # Server-side: footer channel metadata (never exposed to client)
STOREFRONT_DISCOVER_CHANNELS=true # Opt-in: discover ALL active Saleor channels from API
# (not recommended when Saleor has many channels; prefer STOREFRONT_CHANNELS)Channel resolution order (getStorefrontChannelSlugs):
STOREFRONT_CHANNELSβ explicit allowlist (recommended)STOREFRONT_DISCOVER_CHANNELS=true+SALEOR_APP_TOKENβ all active channels from APINEXT_PUBLIC_DEFAULT_CHANNELonly β single-channel storefront
The checkout architecture supports Saleor payment apps like Adyen and Stripe. The heavy lifting is doneβintegrating your gateway requires minimal work compared to building from scratch.
Paper works as a reference implementation and as a starting point for your own storefront. Start here:
- Colors & typography β
src/styles/brand.css - Components β
src/ui/components/ - Checkout flow β
src/checkout/views/saleor-checkout/
The design token system uses CSS custom propertiesβswap the entire color palette by editing a few lines.
Known gaps and planned improvements:
- Checkout functional i18n β checkout step labels still live in storefront content (ADR 0002); migrate to next-intl
- Filtering logic iteration β fetching attributes from API for dynamic product filters
- Error / not-found pages β localized shells for global error boundaries
FSL-1.1-ALv2 (Functional Source License, Version 1.1, ALv2 Future License) β use it, modify it, ship it. Build your storefront, run your business. The license converts to Apache 2.0 after two years.
Want to offer it as a managed service? Let's talk.
Built with π€ by the Saleor team