Platform Evaluation: Saleor vs Medusa v2

Date: 2026-03-12 Context: Dungeon Books — indie sci-fi/fantasy/RPG bookstore, Jersey City NJ Decision: Which commerce platform to build on for the next 12 months

Starters Evaluated

StarterPlatformFrameworkLOCVerdict
Saleor “Paper”SaleorNext.js 16~24,500Primary Saleor option
Official MedusaMedusa v2Next.js 15~13,400Baseline — too bare
SolaceMedusa v2Next.js 16~21,500Strong contender
FashionMedusa v2Next.js 15~15,900Best search, good accounts
MunchiesMedusa v2Astro 5~12,300Wrong framework, no accounts
Medusa POSMedusa v2Expo/RN~9,700POS-only, not relevant

Munchies and Medusa POS are set aside. Munchies uses Astro (not Next.js), deploys to Cloudflare Workers (not Railway), and has no customer accounts. Medusa POS is a React Native mobile app that replaces Square rather than integrating with it.

The real comparison is Saleor “Paper” vs the best Medusa v2 starter.

Which Medusa Starter?

Neither Solace nor Fashion is perfect alone. They complement each other:

FeatureSolaceFashion
SearchBasic (Medusa API, no autocomplete)MeiliSearch with autocomplete
FilteringCollection, type, price rangesCollection, category, type
Image galleryGrid + carousel + lightboxCarousel only, no lightbox
Customer accountsFull + forgot/reset passwordFull + forgot/reset password
Checkout paymentsStripe + PayPalStripe + PayPal
CMS integrationStrapi (blog, about, FAQ, terms)None (hardcoded content)
Dark modeYesNo
BlogYes (categories, MDX)No
Email templatesNoYes (Resend + React Email)
Backend modulesStandard MedusaFashion module + MeiliSearch module
E2E testsPlaywrightPlaywright
UI libraryRadix + Headless UIReact Aria (Adobe)

Recommendation: Start with Solace, adopt Fashion’s MeiliSearch module and search UI.

Solace is the better starting point because it has more complete storefront features (CMS content pages, blog, lightbox gallery, dark mode). Fashion’s key advantage — MeiliSearch with autocomplete — is a backend module + one component (SearchField.tsx, 177 lines) that can be ported into the Solace storefront. The reverse (porting Solace’s CMS integration, blog, lightbox, and dark mode into Fashion) would be significantly more work.

Head-to-Head: Saleor “Paper” vs Solace+MeiliSearch (Medusa v2)

DimensionSaleor “Paper”Solace (Medusa v2)
Lines of code~24,500~21,500
File count316348
Pages out of the box16 + 5 API routes22 + 1 API route
CheckoutMulti-step, dummy paymentMulti-step, real Stripe + PayPal
Express checkoutDecorative buttons onlyNone
SearchBasic (Saleor GraphQL, no autocomplete)Basic (+ MeiliSearch with autocomplete from Fashion module)
Faceted filteringPrice, category, color, size, sortCollection, type, price ranges, sort
Image galleryCarousel (Embla, thumbnails, dots)Grid + carousel + lightbox dialog
Customer accountsFull (password reset, account deletion)Full (password reset, forgot password)
SEOComprehensive (JSON-LD, OG, Twitter, dynamic OG images)Basic (OG tags, sitemap, no JSON-LD)
CMS pagesEditorJS content pagesStrapi (blog, about, FAQ, terms, privacy)
BlogNoYes (categories, sort, search, MDX)
Dark modeYesYes
Theming/reskinEasy (~1-2 files, CSS vars)Moderate-easy (~1 file for colors)
Payment providersDummy (need Saleor Payment App)Stripe + PayPal working
E2E testsNoYes (Playwright)
Email templatesNo (need Saleor App)Available from Fashion starter (Resend)

Integration Complexity

IntegrationSaleorMedusa v2
Ingram enrichmentM — ~6 files, ~400 LOC, 1 extra serviceS — ~3 files, ~250 LOC, 0 extra services
Ingram search fallbackM — ~4 files, ~300 LOCM — ~4 files, ~300 LOC
Square POS syncL — ~10 files, ~600 LOC, 1 extra serviceM — ~7 files, ~450 LOC, 0 extra services
Payment setupM — need Saleor Payment App (Stripe)Already done (Stripe + PayPal ship in Solace)
Total integration LOC~1,600 across ~20 files~1,000 across ~14 files
Extra services for integrations2 (Ingram App + Square App) + 1 (Payment App)0

Deployment Footprint

ServiceSaleorMedusa v2
Commerce backendSaleor API (Python/Django)Medusa server (Node.js)
StorefrontNext.jsNext.js
Search— (built-in)MeiliSearch instance
Ingram integrationSaleor App (standalone service)In-process (subscriber)
Square syncSaleor App (standalone service)In-process (subscriber)
PaymentSaleor App (standalone service)In-process (Stripe provider)
DatabasePostgreSQLPostgreSQL
CacheRedis
Total Railway services5-64 (Medusa, Storefront, MeiliSearch, Redis)

Extensibility: Integration Implementation Outlines

Ingram Product Enrichment on Creation

Saleor

Requires a standalone Saleor App (separate Next.js service):

saleor-ingram-app/
├── src/pages/api/
│   ├── manifest.ts              # App manifest + permissions
│   ├── register.ts              # App registration handler
│   └── webhooks/
│       └── product-created.ts   # PRODUCT_CREATED webhook handler
├── lib/
│   ├── ingram-client.ts         # Rate-limited Ingram API client
│   └── saleor-client.ts         # GraphQL mutations back to Saleor

Flow: Staff creates product with ISBN → Saleor fires PRODUCT_CREATED webhook → App queries Ingram → App calls productUpdate mutation → Product saved as unpublished draft.

~6 files, ~400 LOC, 1 additional deployed service.

Medusa v2

Subscriber + service inside the Medusa backend (same process):

medusa-backend/src/
├── subscribers/
│   └── product-created.ts       # Listens for product.created
├── services/
│   └── ingram.ts                # Rate-limited Ingram API client

Flow: Staff creates product with ISBN → Event bus fires product.created → Subscriber calls IngramService → Updates product via ProductModuleService → Product status: "draft" by default.

~3 files, ~250 LOC, 0 additional services.

Ingram Search Fallback

Both platforms now have search infrastructure. The pattern is similar:

  1. Customer searches → query hits local search first (Saleor GraphQL / MeiliSearch)
  2. If results < threshold → query Ingram Web Service
  3. Display Ingram results as “Available to Order” with distinct styling
  4. Staff approval mechanism to add Ingram products to local catalog

Saleor

Extend src/lib/search/saleor-provider.ts with a composite provider that falls back to an API route proxying Ingram requests.

Medusa v2

Add custom API route src/api/store/search-ingram/route.ts in Medusa backend. Storefront calls MeiliSearch directly for local results, then calls Ingram endpoint for fallback. Reuse existing search UI components with an “Available to Order” variant.

Both: ~4 files, ~300 LOC. Complexity M.

With MeiliSearch, the Medusa approach has an advantage: MeiliSearch provides relevance-scored results out of the box, making it easier to determine when “few results” triggers the Ingram fallback (vs Saleor’s basic GraphQL search which just does substring matching).

Square POS Inventory Sync

Saleor

Separate Saleor App:

saleor-square-sync-app/
├── src/pages/api/
│   ├── webhooks/
│   │   ├── order-created.ts     # Saleor order → decrement Square
│   │   └── stock-updated.ts     # Saleor stock change → update Square
│   ├── square-webhook.ts        # Square webhook → update Saleor
│   └── reconcile.ts             # Nightly cron endpoint
├── lib/
│   ├── square-client.ts
│   ├── saleor-client.ts
│   ├── sync-lock.ts             # Idempotency / loop prevention
│   └── isbn-mapper.ts

~10 files, ~600 LOC, 1 additional service.

Medusa v2

In-process modules and subscribers:

medusa-backend/src/
├── modules/square-sync/
│   ├── service.ts               # SquareSyncService
│   └── models/sync-log.ts       # Sync state tracking
├── subscribers/
│   ├── order-placed.ts          # Order → decrement Square
│   └── inventory-updated.ts     # Inventory change → update Square
├── api/webhooks/square/
│   └── route.ts                 # Square webhook receiver
├── jobs/
│   └── nightly-reconcile.ts     # Scheduled reconciliation

~7 files, ~450 LOC, 0 additional services.

Payload CMS Data Merging

For a “Staff Picks” page merging Payload editorial content with live commerce data:

Saleor (GraphQL)

// Fetch editorial content first (need slugs for commerce query)
const picks = await fetch(`${PAYLOAD_URL}/api/staff-picks?depth=1`)
  .then(r => r.json());
 
// Batch fetch commerce data via GraphQL
const { data } = await executePublicGraphQL(ProductsBySlugDocument, {
  channel, slugs: picks.docs.map(p => p.productSlug),
});
 
// Merge
const merged = picks.docs.map(pick => ({
  ...pick,
  product: data.products.edges.find(e => e.node.slug === pick.productSlug)?.node,
}));

GraphQL allows requesting exactly the needed fields in one batched query.

Medusa v2 (REST + SDK)

const picks = await fetch(`${PAYLOAD_URL}/api/staff-picks?depth=1`)
  .then(r => r.json());
 
// Fetch by handles — can use list endpoint with filter
const { products } = await sdk.client.fetch('/store/products', {
  query: {
    handle: picks.docs.map(p => p.productHandle),
    region_id: region.id,
    fields: 'title,thumbnail,variants.calculated_price',
  },
});
 
// Merge
const merged = picks.docs.map(pick => ({
  ...pick,
  product: products.find(p => p.handle === pick.productHandle),
}));

Verdict: Both patterns are ~20 lines. GraphQL is marginally cleaner for field selection. In practice, no meaningful difference.

Recommendation

Medusa v2 with Solace starter + Fashion’s MeiliSearch module

The single strongest reason: 2 fewer services to deploy and maintain, compounding over 12 months.

Every custom integration (Ingram enrichment, Ingram search fallback, Square sync, payment processing) runs in-process with the Medusa server. With Saleor, each requires a standalone web application — its own deployment, environment config, health checks, log aggregation, and webhook registration. For a solo developer on Railway, the difference between monitoring 4 services vs 6+ is significant.

What Saleor still does better

  • JSON-LD structured data: Important for book rich snippets in Google. Budget half a day to add this to Solace (~100 lines).
  • Attribute-based filtering: Saleor dynamically extracts color/size from product attributes. Solace’s filters are limited to collection/type/price. For books, you’d add genre/author/publisher filters — Medusa’s module system supports this but requires custom work.
  • Dynamic OG images: Saleor generates them server-side. Nice-to-have, not critical. Fashion starter could provide this pattern if needed.

Pre-launch budget on Medusa+Solace

TaskEffort
Port MeiliSearch module + SearchField from Fashion starter1-2 days
Replace Strapi references with Payload API calls1 day
Add JSON-LD Product structured dataHalf day
Adjust price filter buckets for books (15-30, $30+)1 hour
Swap Solace branding (logo, colors, copy)Half day
Verify Stripe + PayPal checkout works end-to-endHalf day
Total~4-5 days

What would tip it to Saleor

  • If the Saleor Dashboard’s built-in product management UI is significantly better for book catalog management (worth testing)
  • If you discover Saleor has a pre-built Ingram or book distributor integration in its App Store
  • If the JSON-LD and SEO gap turns out to be deeper than estimated

Decision

Go with Medusa v2 + Solace starter.

Appendix: Individual Audit Reports