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
| Starter | Platform | Framework | LOC | Verdict |
|---|---|---|---|---|
| Saleor “Paper” | Saleor | Next.js 16 | ~24,500 | Primary Saleor option |
| Official Medusa | Medusa v2 | Next.js 15 | ~13,400 | Baseline — too bare |
| Solace | Medusa v2 | Next.js 16 | ~21,500 | Strong contender |
| Fashion | Medusa v2 | Next.js 15 | ~15,900 | Best search, good accounts |
| Munchies | Medusa v2 | Astro 5 | ~12,300 | Wrong framework, no accounts |
| Medusa POS | Medusa v2 | Expo/RN | ~9,700 | POS-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:
| Feature | Solace | Fashion |
|---|---|---|
| Search | Basic (Medusa API, no autocomplete) | MeiliSearch with autocomplete |
| Filtering | Collection, type, price ranges | Collection, category, type |
| Image gallery | Grid + carousel + lightbox | Carousel only, no lightbox |
| Customer accounts | Full + forgot/reset password | Full + forgot/reset password |
| Checkout payments | Stripe + PayPal | Stripe + PayPal |
| CMS integration | Strapi (blog, about, FAQ, terms) | None (hardcoded content) |
| Dark mode | Yes | No |
| Blog | Yes (categories, MDX) | No |
| Email templates | No | Yes (Resend + React Email) |
| Backend modules | Standard Medusa | Fashion module + MeiliSearch module |
| E2E tests | Playwright | Playwright |
| UI library | Radix + Headless UI | React 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)
| Dimension | Saleor “Paper” | Solace (Medusa v2) |
|---|---|---|
| Lines of code | ~24,500 | ~21,500 |
| File count | 316 | 348 |
| Pages out of the box | 16 + 5 API routes | 22 + 1 API route |
| Checkout | Multi-step, dummy payment | Multi-step, real Stripe + PayPal |
| Express checkout | Decorative buttons only | None |
| Search | Basic (Saleor GraphQL, no autocomplete) | Basic (+ MeiliSearch with autocomplete from Fashion module) |
| Faceted filtering | Price, category, color, size, sort | Collection, type, price ranges, sort |
| Image gallery | Carousel (Embla, thumbnails, dots) | Grid + carousel + lightbox dialog |
| Customer accounts | Full (password reset, account deletion) | Full (password reset, forgot password) |
| SEO | Comprehensive (JSON-LD, OG, Twitter, dynamic OG images) | Basic (OG tags, sitemap, no JSON-LD) |
| CMS pages | EditorJS content pages | Strapi (blog, about, FAQ, terms, privacy) |
| Blog | No | Yes (categories, sort, search, MDX) |
| Dark mode | Yes | Yes |
| Theming/reskin | Easy (~1-2 files, CSS vars) | Moderate-easy (~1 file for colors) |
| Payment providers | Dummy (need Saleor Payment App) | Stripe + PayPal working |
| E2E tests | No | Yes (Playwright) |
| Email templates | No (need Saleor App) | Available from Fashion starter (Resend) |
Integration Complexity
| Integration | Saleor | Medusa v2 |
|---|---|---|
| Ingram enrichment | M — ~6 files, ~400 LOC, 1 extra service | S — ~3 files, ~250 LOC, 0 extra services |
| Ingram search fallback | M — ~4 files, ~300 LOC | M — ~4 files, ~300 LOC |
| Square POS sync | L — ~10 files, ~600 LOC, 1 extra service | M — ~7 files, ~450 LOC, 0 extra services |
| Payment setup | M — 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 integrations | 2 (Ingram App + Square App) + 1 (Payment App) | 0 |
Deployment Footprint
| Service | Saleor | Medusa v2 |
|---|---|---|
| Commerce backend | Saleor API (Python/Django) | Medusa server (Node.js) |
| Storefront | Next.js | Next.js |
| Search | — (built-in) | MeiliSearch instance |
| Ingram integration | Saleor App (standalone service) | In-process (subscriber) |
| Square sync | Saleor App (standalone service) | In-process (subscriber) |
| Payment | Saleor App (standalone service) | In-process (Stripe provider) |
| Database | PostgreSQL | PostgreSQL |
| Cache | — | Redis |
| Total Railway services | 5-6 | 4 (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:
- Customer searches → query hits local search first (Saleor GraphQL / MeiliSearch)
- If results < threshold → query Ingram Web Service
- Display Ingram results as “Available to Order” with distinct styling
- 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
| Task | Effort |
|---|---|
| Port MeiliSearch module + SearchField from Fashion starter | 1-2 days |
| Replace Strapi references with Payload API calls | 1 day |
| Add JSON-LD Product structured data | Half 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-end | Half 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
- Saleor “Paper” Audit
- Medusa Official Starter Audit
- Solace Starter Audit
- Fashion Starter Audit
- Munchies Starter Audit
- Medusa POS Starter Audit
- medusa-pos-starter — Agilo POS starter reference notes