
Saleor Development
- 58 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Build and extend Saleor's GraphQL headless commerce platform with custom apps, webhook handlers, and dashboard UI customizations.
About
Extends the Saleor GraphQL headless commerce platform through custom apps, webhook handlers, and dashboard UI changes. A developer uses it when building on or customizing a Saleor backend.
- Custom Saleor apps and webhook handlers
- Dashboard UI customization on a GraphQL backend
Saleor Development by the numbers
- 58 all-time installs (skills.sh)
- Ranked #3,178 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill saleor-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 58 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Build and extend Saleor's GraphQL headless commerce platform with custom apps, webhook handlers, and dashboard UI customizations.
Files
Saleor Development
Overview
Saleor is a headless, GraphQL-first e-commerce platform built on Django and Python. It exposes a fully typed GraphQL API for storefronts and third-party apps, a React-based dashboard for store management, and an extension system that lets you react to events via webhooks or inject UI into the dashboard. This skill covers querying the Saleor API, building Saleor Apps (plugins hosted outside Saleor), and customizing the dashboard with App Extensions.
When to Use This Skill
- When building a custom storefront (Next.js, Remix, mobile) against a Saleor backend
- When creating a Saleor App that reacts to order or product lifecycle webhooks
- When injecting custom UI panels into the Saleor Dashboard via App Extensions
- When exploring or extending the Saleor product catalog, checkout, or customer APIs
- When setting up a local Saleor development environment with Docker
Prerequisites & Platform Notes
This skill is written for custom/headless storefronts (Node.js, Python, or similar backend). The code examples use TypeScript/Node.js and can be adapted to any stack.
Shopify: Shopify Hydrogen is Shopify's headless framework. MACH/composable patterns apply when using Shopify as the commerce backend with a custom frontend, or when mixing Shopify with other best-of-breed services. WooCommerce: WooCommerce can serve as a headless backend via its REST API and WPGraphQL. These patterns apply when decoupling the frontend from WordPress. Magento: Magento's GraphQL API and PWA Studio support headless architectures. These composable patterns apply to Magento as a backend service in a MACH stack.
You'll need:
- Node.js 18+ (or adapt to your backend language)
- PostgreSQL (or your preferred relational database)
- Redis for caching/queues
- Stripe account and API keys
- An email sending service (SendGrid, AWS SES, or Postmark)
- Docker and/or Kubernetes for container orchestration
- CDN (Cloudflare, CloudFront, or Fastly)
Core Instructions
1. Run Saleor locally with Docker Compose
git clone https://github.com/saleor/saleor-platform.git
cd saleor-platform
docker compose up --detach
# API: http://localhost:8000/graphql/
# Dashboard: http://localhost:9000Create the first superuser and populate demo data:
docker compose run --rm api python manage.py createsuperuser
docker compose run --rm api python manage.py populatedb --createsuperuser2. Query the Storefront GraphQL API
Use the Saleor CLI or any GraphQL client (Apollo, urql, graphql-request).
Install the CLI for code generation:
npm install -g @saleor/cli
saleor configureExample — fetch the first 12 products from the default channel:
query ProductList($channel: String!) {
products(first: 12, channel: $channel) {
edges {
node {
id
name
slug
thumbnail { url alt }
pricing {
priceRange {
start { gross { amount currency } }
}
}
}
}
pageInfo { hasNextPage endCursor }
}
} import { createClient } from 'urql';
const client = createClient({
url: process.env.NEXT_PUBLIC_SALEOR_API_URL,
fetchOptions: () => ({
headers: { 'Content-Type': 'application/json' },
}),
});
const { data } = await client.query(PRODUCT_LIST_QUERY, { channel: 'default-channel' }).toPromise();3. Authenticate a customer and start checkout
mutation CustomerLogin($email: String!, $password: String!) {
tokenCreate(email: $email, password: $password) {
token
refreshToken
errors { field message }
user { id email }
}
}Create a checkout and add lines:
mutation CheckoutCreate($channel: String!, $lines: [CheckoutLineInput!]!) {
checkoutCreate(input: { channel: $channel, lines: $lines }) {
checkout {
id
token
totalPrice { gross { amount currency } }
}
errors { field message }
}
}Complete checkout with a payment gateway token (e.g., from Stripe Elements):
mutation CheckoutComplete($checkoutId: ID!, $paymentData: JSONString) {
checkoutComplete(id: $checkoutId, paymentData: $paymentData) {
order { id number status }
errors { field message code }
}
}4. Bootstrap a Saleor App
A Saleor App is a Node.js service that registers itself with Saleor, receives webhooks, and optionally renders UI in the dashboard via iframes.
npx @saleor/app-sdk@latest create my-saleor-app
cd my-saleor-app
npm install
npm run dev
# Expose with: npx ngrok http 3000Register the app in the dashboard under Apps → Install custom app, entering your ngrok URL. Saleor calls your /api/manifest endpoint:
// pages/api/manifest.ts
import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
import { AppManifest } from "@saleor/app-sdk/types";
const manifest: AppManifest = {
id: "my-saleor-app",
name: "My Saleor App",
version: "1.0.0",
about: "Example app",
permissions: ["MANAGE_ORDERS"],
appUrl: process.env.APP_URL!,
tokenTargetUrl: `${process.env.APP_URL}/api/register`,
webhooks: [
{
name: "Order Created",
asyncEvents: ["ORDER_CREATED"],
query: `subscription { event { ... on OrderCreated { order { id number } } } }`,
targetUrl: `${process.env.APP_URL}/api/webhooks/order-created`,
isActive: true,
},
],
};
export default createManifestHandler({ manifestFactory: () => manifest });5. Handle Saleor webhooks securely
Saleor signs every webhook with an HMAC-SHA256 signature using your app's secret token.
// pages/api/webhooks/order-created.ts
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { OrderCreatedDocument } from "@/generated/graphql";
const orderCreatedWebhook = new SaleorAsyncWebhook<OrderCreatedPayload>({
name: "Order Created",
webhookPath: "api/webhooks/order-created",
asyncEvent: "ORDER_CREATED",
apl: saleorApp.apl,
query: OrderCreatedDocument,
});
export default orderCreatedWebhook.createHandler((req, res, ctx) => {
const { order } = ctx.payload;
console.log(`New order #${order.number} received`);
// Trigger fulfillment, email, ERP sync, etc.
return res.status(200).end();
});
export const config = { api: { bodyParser: false } }; // required for signature check6. Add a Dashboard Extension (custom UI panel)
Extensions render an iframe inside the Saleor Dashboard. Declare them in the manifest:
extensions: [
{
label: "Sync to ERP",
mount: "PRODUCT_DETAILS_MORE_ACTIONS",
target: "POPUP",
permissions: ["MANAGE_PRODUCTS"],
url: `${process.env.APP_URL}/extension/product-sync`,
},
],The extension page uses @saleor/app-sdk to communicate with the dashboard host:
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
export default function ProductSyncExtension() {
const { appBridge } = useAppBridge();
const handleSync = async () => {
appBridge?.dispatch(actions.Notification({
status: "success",
title: "Sync started",
text: "Product is being synced to ERP.",
}));
};
return <button onClick={handleSync}>Sync to ERP</button>;
}Examples
Paginated product catalog with TypeScript and graphql-request
import { GraphQLClient, gql } from 'graphql-request';
const client = new GraphQLClient(process.env.SALEOR_API_URL!, {
headers: { Authorization: `Bearer ${process.env.SALEOR_APP_TOKEN}` },
});
const PRODUCTS_QUERY = gql`
query Products($first: Int!, $after: String, $channel: String!) {
products(first: $first, after: $after, channel: $channel) {
edges { node { id name slug description } }
pageInfo { hasNextPage endCursor }
}
}
`;
async function fetchAllProducts(channel: string) {
const products = [];
let after: string | null = null;
do {
const data = await client.request(PRODUCTS_QUERY, { first: 100, after, channel });
products.push(...data.products.edges.map((e: any) => e.node));
after = data.products.pageInfo.hasNextPage ? data.products.pageInfo.endCursor : null;
} while (after);
return products;
}Order status update via Admin API
mutation FulfillOrder($orderId: ID!, $input: OrderFulfillInput!) {
orderFulfill(orderId: $orderId, input: $input) {
fulfillments {
id
status
trackingNumber
}
errors { field message code }
}
}await client.request(FULFILL_ORDER_MUTATION, {
orderId: "T3JkZXI6MTIz",
input: {
lines: [{ orderLineId: "T3JkZXJMaW5lOjQ1", stocks: [{ warehouse: "V2FyZWhvdXNlOjE=", quantity: 1 }] }],
notifyCustomer: true,
allowStockToBeExceeded: false,
},
});Best Practices
- Use channels for multi-region or B2B/B2C separation — every product listing, pricing, and checkout is channel-scoped; create separate channels per locale/currency rather than duplicating products
- Generate TypeScript types from the schema — run
saleor app generate-typesor usegraphql-codegenso queries are fully typed - Store app tokens in Saleor's APL (Auth Persistence Layer) — the default file-based APL is fine for development; use Redis or Upstash APL in production
- Always verify webhook signatures — use the
SaleorAsyncWebhookwrapper which handles HMAC verification automatically; never process unauthenticated payloads - Use subscription-based webhook queries — Saleor webhooks use GraphQL subscriptions as the payload definition, giving you control over exactly which fields are included
- Cache product catalog responses at the CDN layer — product data rarely changes; set
Cache-Control: s-maxage=300on catalog API routes - Use Saleor Cloud for production — self-hosting Django + Celery + Redis + PostgreSQL requires operational maturity; Saleor Cloud handles this
Common Pitfalls
| Problem | Solution |
|---|---|
| GraphQL errors for unauthorized operations | Ensure the app has been granted the correct permissions in the manifest AND in the dashboard under App settings |
| Webhook payload is empty / fields missing | The webhook payload is defined by a GraphQL subscription query in the manifest — add the fields you need to the query property |
tokenCreate returns null on storefront | The channel must have the storefront API enabled and an assigned country; check channel configuration in the dashboard |
| App works locally but not after deployment | The APP_URL env var must match the publicly accessible URL Saleor can reach; update the app URL in the dashboard after deployment |
| Dashboard extension iframe is blank | The extension URL must be served over HTTPS and must include Access-Control-Allow-Origin headers for the dashboard origin |
Related Skills
- @shopify-hydrogen
- @composable-commerce
- @webhook-architecture
- @jamstack-storefront
- @commerce-api-gateway
{
"context": "Tests whether the agent correctly declares a Dashboard Extension in the Saleor App manifest with the required fields, uses the app-bridge hooks from @saleor/app-sdk to communicate with the dashboard host, dispatches a Notification action, and documents the HTTPS and CORS production requirements.",
"type": "weighted_checklist",
"checklist": [
{
"name": "extensions array in manifest",
"max_score": 8,
"description": "The AppManifest object in pages/api/manifest.ts contains an `extensions` array (not undefined or absent)"
},
{
"name": "Extension mount point",
"max_score": 10,
"description": "The extension entry includes a `mount` property set to a valid Saleor dashboard mount point (e.g., PRODUCT_DETAILS_MORE_ACTIONS or similar PRODUCT_* mount)"
},
{
"name": "Extension target field",
"max_score": 8,
"description": "The extension entry includes a `target` property (e.g., POPUP or APP_PAGE)"
},
{
"name": "Extension permissions",
"max_score": 8,
"description": "The extension entry includes a `permissions` array containing at least one permission (e.g., MANAGE_PRODUCTS)"
},
{
"name": "Extension URL from env",
"max_score": 8,
"description": "The extension `url` property is constructed using process.env.APP_URL (or equivalent env var) — not hardcoded"
},
{
"name": "useAppBridge import",
"max_score": 10,
"description": "pages/extension/inventory-reserve.tsx imports useAppBridge from @saleor/app-sdk/app-bridge"
},
{
"name": "actions import",
"max_score": 8,
"description": "pages/extension/inventory-reserve.tsx imports actions from @saleor/app-sdk/app-bridge"
},
{
"name": "Notification dispatch",
"max_score": 12,
"description": "The extension page calls appBridge.dispatch(actions.Notification({ ... })) or appBridge?.dispatch(actions.Notification({ ... })) to send a notification to the dashboard"
},
{
"name": "HTTPS requirement documented",
"max_score": 8,
"description": "README.md (or inline comments) explicitly states that the extension URL must be served over HTTPS in production"
},
{
"name": "CORS requirement documented",
"max_score": 10,
"description": "README.md (or inline comments) explicitly states that the extension page must include Access-Control-Allow-Origin headers for the dashboard origin"
},
{
"name": "createManifestHandler used",
"max_score": 10,
"description": "pages/api/manifest.ts exports a handler created with createManifestHandler from @saleor/app-sdk/handlers/next"
}
]
}
Saleor Dashboard Extension: Inventory Reservation Button
Problem/Feature Description
A wholesale distributor manages their product catalog in Saleor and also maintains an external warehouse management system (WMS). Their operations team spends time manually copying SKUs and stock levels between the two systems. To reduce this friction, the engineering team wants to embed a "Reserve Inventory" button directly inside the Saleor product details page. When a staff member clicks the button, it should trigger a reservation request to the WMS and show a success or failure notification within the Saleor Dashboard — without navigating away.
The team has already built a Saleor App skeleton (manifest and registration endpoint exist). They now need to extend the manifest to declare the Dashboard Extension and build the React page that renders inside the dashboard iframe. The extension should be accessible from the product details page and communicate with the dashboard shell to display status notifications. A README or inline comments should describe the HTTPS and CORS requirements for the extension page to work in production.
Output Specification
Produce the following files:
pages/api/manifest.ts— updated manifest handler that declares the Dashboard Extensionpages/extension/inventory-reserve.tsx— the React page rendered inside the extension iframeREADME.md— brief notes on deployment requirements for the extension to work correctly in production (HTTPS, CORS)
The extension page should simulate the WMS call (a placeholder async function is fine) and show a notification in the dashboard on completion. Do not implement real WMS API calls — a mock/stub is sufficient.
{
"context": "Tests whether the agent correctly bootstraps a Saleor App using the official app-sdk, declares the manifest with all required fields, uses subscription-based webhook payload queries, and handles incoming webhooks securely via the SaleorAsyncWebhook wrapper.",
"type": "weighted_checklist",
"checklist": [
{
"name": "app-sdk dependency",
"max_score": 8,
"description": "package.json includes @saleor/app-sdk as a dependency (any version)"
},
{
"name": "createManifestHandler import",
"max_score": 8,
"description": "pages/api/manifest.ts imports createManifestHandler from @saleor/app-sdk/handlers/next"
},
{
"name": "tokenTargetUrl field",
"max_score": 10,
"description": "The manifest object includes a tokenTargetUrl property pointing to the /api/register path (e.g., `${process.env.APP_URL}/api/register` or equivalent)"
},
{
"name": "Webhook subscription query",
"max_score": 10,
"description": "The webhook entry in the manifest uses a GraphQL subscription query (starts with 'subscription {' or uses a subscription document) as the payload definition, not a plain fragment or empty string"
},
{
"name": "ORDER_CREATED async event",
"max_score": 8,
"description": "The webhook in the manifest declares asyncEvents: [\"ORDER_CREATED\"] (or equivalent enum value)"
},
{
"name": "SaleorAsyncWebhook usage",
"max_score": 12,
"description": "pages/api/webhooks/order-created.ts instantiates SaleorAsyncWebhook (imported from @saleor/app-sdk/handlers/next) rather than manually parsing the request body or implementing custom HMAC verification"
},
{
"name": "bodyParser disabled",
"max_score": 12,
"description": "pages/api/webhooks/order-created.ts exports `export const config = { api: { bodyParser: false } }` (exact pattern required for signature verification)"
},
{
"name": "APL referenced",
"max_score": 8,
"description": "The SaleorAsyncWebhook constructor receives an `apl` property referencing the app's APL instance (not hardcoded or omitted)"
},
{
"name": "Manifest permissions",
"max_score": 8,
"description": "The manifest declares a permissions array containing at least one relevant permission (e.g., MANAGE_ORDERS)"
},
{
"name": "createHandler used",
"max_score": 8,
"description": "The webhook handler uses orderCreatedWebhook.createHandler(...) (or equivalent SaleorAsyncWebhook method) to export the handler, rather than exporting a plain async function"
},
{
"name": "Order fields in payload",
"max_score": 8,
"description": "The subscription query in the webhook definition requests at least id and number fields from the order"
}
]
}
Order Event Processor for Saleor
Problem/Feature Description
A logistics startup has recently integrated Saleor as their e-commerce backend and needs to build a lightweight service that reacts in real-time when orders are placed. Whenever a customer completes checkout, the service should receive the order data from Saleor and hand it off to their internal fulfillment pipeline. The engineering team has decided to build a dedicated Saleor App — a Node.js service that lives outside of Saleor itself and communicates with it via the app registration protocol and webhook events.
The team is starting from scratch and wants a minimal but correct implementation. They need the app manifest, the registration endpoint, and a working webhook handler that fires when an order is placed. Security is a priority: the service must authenticate incoming webhook payloads rather than blindly trusting them. The handler should log enough order details to hand off to the fulfillment pipeline.
Output Specification
Write the TypeScript source files for a Next.js-based Saleor App. Produce the following files in your working directory:
package.json— dependencies needed to run the apppages/api/manifest.ts— the app manifest handlerpages/api/register.ts— the token registration endpointpages/api/webhooks/order-created.ts— the order creation webhook handler
For each file include brief inline comments explaining key decisions.
{
"context": "Tests whether the agent correctly scopes all product queries to a Saleor channel, implements cursor-based pagination, sets CDN cache headers on catalog routes, uses an appropriate GraphQL client, adds typed queries via codegen, and authenticates Admin API calls with a Bearer token.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Channel parameter required",
"max_score": 10,
"description": "The products GraphQL query declares $channel as a required variable (e.g., `$channel: String!`) and passes it to the products() call"
},
{
"name": "Channel value supplied",
"max_score": 8,
"description": "The fetchAllProducts function accepts a channel argument and passes it when executing the query — does NOT hardcode 'default-channel' or omit the channel"
},
{
"name": "Cursor pagination loop",
"max_score": 12,
"description": "fetchAllProducts uses a loop (do/while or while) that checks pageInfo.hasNextPage and passes pageInfo.endCursor as the `after` variable on subsequent requests"
},
{
"name": "pageInfo fields requested",
"max_score": 8,
"description": "The products query includes `pageInfo { hasNextPage endCursor }` in the selection set"
},
{
"name": "GraphQL client library used",
"max_score": 8,
"description": "The client in lib/saleorClient.ts uses one of: urql (createClient), graphql-request (GraphQLClient), or Apollo Client — does NOT use raw fetch() with manual JSON stringification"
},
{
"name": "Authorization Bearer header",
"max_score": 8,
"description": "The GraphQL client or individual requests include an `Authorization: Bearer <token>` header (reading the token from an environment variable)"
},
{
"name": "Cache-Control header set",
"max_score": 12,
"description": "pages/api/catalog.ts sets a Cache-Control header with `s-maxage=300` (or a value including `s-maxage`) on the response"
},
{
"name": "TypeScript types for queries",
"max_score": 10,
"description": "The code includes either a graphql-codegen config file OR explicit TypeScript interfaces/types for the product query response — queries are NOT typed as `any`"
},
{
"name": "Environment variable for API URL",
"max_score": 8,
"description": "The Saleor API URL is read from an environment variable (NEXT_PUBLIC_SALEOR_API_URL or similar) — not hardcoded as a string literal"
},
{
"name": "urql Content-Type header",
"max_score": 8,
"description": "If urql is used, the createClient fetchOptions includes `'Content-Type': 'application/json'` in the headers"
},
{
"name": "No offset-based pagination",
"max_score": 8,
"description": "The pagination implementation does NOT use offset/skip-based pagination — only cursor-based (after + endCursor) is used"
}
]
}
Product Catalog API for a Next.js Storefront
Problem/Feature Description
A fashion retailer is building a new Next.js storefront on top of their Saleor backend. Their marketing team regularly runs promotions for different regions — Europe uses EUR pricing while the US uses USD — so the backend is configured with multiple channels to handle regional pricing and availability. The storefront team needs a reliable way to fetch the full product catalog, including names, slugs, thumbnails, and pricing, so it can power both the homepage grid and a search index.
The catalog contains thousands of products, so a single query won't return everything. The team needs a TypeScript utility that can walk through all pages of results and return the complete list. Performance matters: catalog data barely changes during the day, so the team wants CDN-level caching on the API route that serves this data. They also need the code to be type-safe since the frontend is entirely TypeScript.
Output Specification
Write a TypeScript implementation with the following files:
lib/saleorClient.ts— GraphQL client setup targeting the Saleor APIlib/fetchAllProducts.ts— a function that fetches all products from a given channel, handling large catalogs across multiple pages, returning a typed arraypages/api/catalog.ts— a Next.js API route that calls fetchAllProducts and returns JSON, with appropriate caching headers set on the response
Include a graphql-codegen.yml (or equivalent config) or inline type definitions that show how types are generated from the Saleor schema.
You may use environment variables such as NEXT_PUBLIC_SALEOR_API_URL and SALEOR_APP_TOKEN — do not hardcode URLs or tokens.
{
"name": "finsi/saleor-development",
"version": "0.1.0",
"summary": "Saleor GraphQL API, app development, and dashboard customization",
"skills": {
"saleor-development": {
"path": "SKILL.md"
}
}
}