New Project
Saleor Storefront built with React 18, Next.js 14, App Router, TypeScript, GraphQL, and Tailwind CSS.
A minimal, production-ready storefront template for Saleor.
Clean as a blank page — built to ship.
[!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. We aim to provide open UI components and full wiring around the whole process.
One codebase, many storefronts. Channel-scoped routing means /us/products and /eu/products can serve different catalogs, prices, and shipping options—all from the same deployment.
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.
The hard parts are solved. Adapt the look, keep the logic.
searchParams change.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/ [blocked] — 13 task-specific rules covering GraphQL, caching, variant selection, checkout, and more.agents/skills/ [blocked] — Installed community skills (Vercel React best practices, composition patterns)Whether you're pair-programming with Cursor, Claude, or Copilot—the codebase is designed to help them help you.
| Feature | Description |
|---|---|
| Checkout | Multi-step flow with guest/auth support, address selector, international forms |
| 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 |
| Navigation | Dynamic menus from Saleor, mobile hamburger |
| SEO | Metadata, JSON-LD, Open Graph images |
| Caching | Cache Components (PPR), named cacheLife tiers, channel-scoped tags, webhooks |
| 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 (Partial Prerendering) for optimal performance—static shells load instantly while dynamic content streams in. Learn more in the Next.js documentation or see skills/saleor-paper-storefront/rules/data-caching.md [blocked] for project-specific implementation details.
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: navigation:{channel}, footer-menu:{channel} |
| Cart drawer | Always live | Saleor API with cache: "no-cache" |
| Checkout | Always live | Direct API calls, 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.
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 "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 |
category:{slug} | Category updated |
collection:{slug} | Collection updated |
navigation:{channel} | Main menu changed for channel |
footer-menu:{channel} | Footer menu changed for channel |
channels | Channel list metadata |
Featured homepage products use tag collection:featured-products (same catalog profile as collections).
Configure Saleor webhooks to invalidate cache immediately when data changes:
https://your-store.com/api/revalidateMENU_* / MENU_ITEM_* (Paper app forwards { menu: { slug } } for navbar and footer menus)SALEOR_WEBHOOK_SECRET env varManual revalidation (requires REVALIDATE_SECRET):
# Single productcurl "https://your-store.com/api/revalidate?secret=xxx&tag=product: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 channelcurl "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.
checkoutLinesAdd calculates prices server-sidecheckoutComplete uses real-time data📚 Deep dive: See
skills/saleor-paper-storefront/rules/data-caching.md[blocked] for 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@latestsaleor storefront create --url https://{YOUR_INSTANCE}/graphql/# Or manuallygit clone https://github.com/saleor/storefront.gitcd storefrontcp .env.example .envpnpm install
Edit .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 slug
Multi-channel (recommended — explicit allowlist):
STOREFRONT_CHANNELS=us,uk,euNEXT_PUBLIC_DEFAULT_CHANNEL=usSALEOR_APP_TOKEN=... # Server-side only — footer currency selector metadata
Finding 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 dev
Open localhost:3000. That's it.
pnpm dev # Start dev serverpnpm build # Production buildpnpm run generate # Regenerate GraphQL types (storefront)pnpm run generate:checkout # Regenerate GraphQL types (checkout)
src/├── app/ # Next.js App Router│ ├── [channel]/ # Channel-scoped routes│ └── checkout/ # Checkout pages├── checkout/ # Checkout components & logic├── 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/ — 13 project-specific rules (GraphQL, caching, checkout, etc.).agents/skills/ — Installed community skills (React best practices, composition patterns)To install the project skill for agent auto-discovery:
npx skills add . --skill saleor-paper-storefront
# RequiredNEXT_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# OptionalNEXT_PUBLIC_STOREFRONT_URL= # Canonical URLs and OG imagesREVALIDATE_SECRET= # Manual cache invalidation (GET /api/revalidate)SALEOR_WEBHOOK_SECRET= # Webhook HMAC verificationSALEOR_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 for large catalogs; 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_CHANNEL only — single-channel storefrontThe 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:
src/styles/brand.csssrc/ui/components/src/checkout/views/SaleorCheckout/The design token system uses CSS custom properties—swap the entire color palette by editing a few lines.
Features planned for future development:
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