Building an online store without surrendering control of your data, customer information, and revenue streams to a SaaS platform is one of the most impactful infrastructure decisions a business can make. Headless e-commerce platforms separate the backend — products, orders, payments, inventory — from the frontend presentation layer, giving you full flexibility over the customer experience while keeping the commerce engine under your control.

In 2026, three open-source platforms dominate this space: Medusa, Saleor, and Vendure. Each takes a different architectural approach, supports different tech stacks, and targets different types of merchants. This guide compares them side by side and provides complete self-hosting instructions for each.

Why Self-Host Your E-Commerce Platform

Running your own e-commerce backend offers advantages that go far beyond cost savings:

  • Zero platform fees. Shopify charges 0.5–2% transaction fees on top of payment processor costs. Self-hosted platforms eliminate this entirely.
  • Full data ownership. Customer data, purchase history, and behavioral analytics stay on your infrastructure. No third-party data sharing, no vendor lock-in, no surprise policy changes.
  • Unlimited customization. Headless architecture means your frontend can be anything — a React SPA, a static Hugo site, a mobile app, or a custom native experience — with no template restrictions.
  • No API rate limits. SaaS platforms throttle API calls on lower-tier plans. Self-hosted, you control the infrastructure and can scale as needed.
  • Regulatory compliance. When you host the data yourself, you control exactly where it lives and how it’s processed — critical for GDPR, CCPA, and industry-specific requirements.
  • Portability. Your store is not tied to a vendor’s ecosystem. You can change hosting providers, swap payment gateways, or modify any component without platform approval.

The trade-off is operational responsibility: you manage deployment, scaling, backups, and updates. For teams with even basic DevOps experience, the returns on investment are substantial.

Platform Overview

Medusa — The Modular JavaScript Platform

Medusa is a Node.js-based headless commerce platform designed around a plugin architecture. It describes itself as “the open-source Shopify alternative” and provides a comprehensive set of commerce primitives out of the box — products, pricing, cart, checkout, orders, returns, and customer management — with the ability to extend virtually any behavior through plugins and modules.

Architecture: Monolithic Node.js backend with pluggable modules. REST API by default, with optional GraphQL through community plugins.

Database: PostgreSQL (required), Redis (recommended for caching and job queues).

Admin dashboard: Built-in React-based admin panel, shipped separately from the core server.

Best for: Developers who want Shopify-level features with complete code-level customization and a large plugin ecosystem.

Saleor — The Python-Powered GraphQL-First Platform

Saleor is a Python (Django + Graphene) headless commerce platform that is GraphQL-first from the ground up. Every operation — product queries, cart mutations, checkout flows — is exposed through a single GraphQL endpoint. Saleor’s architecture is designed for high-performance storefronts and supports multi-channel, multi-currency operations natively.

Architecture: Python/Django core with a GraphQL API layer. Separate dashboard application (React/TypeScript).

Database: PostgreSQL (required), Redis (required for caching and async tasks).

Admin dashboard: React-based dashboard included, with a rich product management interface.

Best for: Teams that want GraphQL-first APIs, strong internationalization (multi-currency, multi-language), and a polished admin experience.

Vendure — The TypeScript GraphQL Platform

Vendure is a headless commerce framework built entirely in TypeScript using NestJS and GraphQL. It emphasizes developer experience with strong typing, a clean plugin API, and a focus on B2B and complex catalog scenarios. Vendure’s architecture is designed to be extended rather than modified — you build on top of its core rather than patching internals.

Architecture: NestJS backend with GraphQL API. Admin UI and storefront starter kits provided separately.

Database: PostgreSQL and MySQL (both supported), with Redis for caching.

Admin dashboard: Angular-based admin UI, fully featured with product, order, customer, and settings management.

Best for: TypeScript/NestJS teams, B2B commerce scenarios, and projects where strong typing and developer experience are priorities.

Feature Comparison

FeatureMedusaSaleorVendure
Backend languageNode.js (JavaScript/TypeScript)Python (Django)TypeScript (NestJS)
Primary APIRESTGraphQLGraphQL
Secondary APIGraphQL (community plugins)REST (limited)
DatabasePostgreSQLPostgreSQLPostgreSQL, MySQL
Admin dashboardReact (separate package)React (included)Angular (included)
Multi-currencyYes (plugin)Yes (native)Yes (native)
Multi-languageYes (plugin)Yes (native)Yes (native)
Multi-channelLimitedYes (native)Yes (native)
Product variantsYesYesYes
Digital productsYes (plugin)YesYes
SubscriptionsYes (plugin)Yes (plugin)Yes (plugin)
B2B featuresLimitedLimitedYes (strong)
Inventory managementYesYesYes
Tax calculationPlugin-basedPlugin-basedPlugin-based
Promotions/DiscountsYesYesYes
Gift cardsYesYesYes
WebhooksYesYesYes
Job queueYes (Redis/Bull)Yes (Celery)Yes (built-in)
Plugin systemModules & pluginsPlugins & webhooksPlugins (NestJS providers)
Storefront startersNext.js, GatsbyReact Storefront, PWANext.js, Astro, React
LicenseMITBSD 3-ClauseMIT
GitHub stars25,000+docker0+5,000+
Docker supportYesYesYes

Self-Hosting Guide: Medusa

Prerequisites

  • Node.js 20+ installed
  • PostgreSQL 15+ running
  • Redis 7+ running
  • npm or pnpm package manager

Step 1: Install the Medusa CLI

1
npm install -g @medusajs/medusa-cli

Step 2: Create a New Medusa Project

1
2
medusa new my-store --skip-db --skip-migrations
cd my-store

Step 3: Configure the Database

Create a PostgreSQL database:

1
2
3
psql -U postgres -c "CREATE DATABASE medusa_store;"
psql -U postgres -c "CREATE USER medusa_user WITH PASSWORD 'your_secure_password';"
psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE medusa_store TO medusa_user;"

Edit .env in your project root:

1
2
3
4
5
6
7
8
DATABASE_URL=postgresql://medusa_user:your_secure_password@localhost:5432/medusa_store
REDIS_URL=redis://localhost:6379
JWT_SECRET=your_jwt_secret_here_64_chars_minimum
COOKIE_SECRET=your_cookie_secret_here_64_chars
NPM_CONFIG_LOGLEVEL=error
STORE_CORS=http://localhost:8000,https://your-storefront.com
ADMIN_CORS=http://localhost:7000,https://your-admin.com
AUTH_CORS=http://localhost:7000,https://your-admin.com

Step 4: Run Migrations and Seed Data

1
2
medusa migrations run
medusa seed

Step 5: Start the Medusa Server and Admin

1
2
3
4
5
6
7
# Terminal 1: Start the backend
medusa develop

# Terminal 2: Start the admin dashboard
cd ../admin
npm install
npm run start

The backend runs on http://localhost:9000 and the admin on http://localhost:7001. Create an admin user at /app/settings/manage-your-store.

Docker Compose Deployment

For production deployment, use Docker Compose:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
version: "3.8"

services:
  medusa:
    image: node:20-alpine
    working_dir: /app
    command: sh -c "npm install && medusa migrations run && medusa develop"
    ports:
      - "9000:9000"
    environment:
      DATABASE_URL: postgresql://medusa:medusa@postgres:5432/medusa
      REDIS_URL: redis://redis:6379
      JWT_SECRET: ${JWT_SECRET}
      COOKIE_SECRET: ${COOKIE_SECRET}
      STORE_CORS: "https://your-store.com"
      ADMIN_CORS: "https://admin.your-store.com"
    volumes:
      - .:/app
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: medusa
      POSTGRES_PASSWORD: medusa
      POSTGRES_DB: medusa
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U medusa"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  admin:
    image: node:20-alpine
    working_dir: /app/admin
    command: sh -c "npm install && npm run build"
    build:
      context: .
      dockerfile: admin/Dockerfile
    ports:
      - "7001:80"
    depends_on:
      - medusa
    restart: unless-stopped

volumes:
  pgdata:

Key Medusa plugins to consider for a production store:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Payment processing
npm install @medusajs/medusa-payment-stripe

# Search functionality
npm install medusa-plugin-meilisearch

# Email notifications
npm install @medusajs/medusa-plugin-sendgrid

# File storage (S3)
npm install @medusajs/medusa-file-s3

Self-Hosting Guide: Saleor

Prerequisites

  • Docker and Docker Compose
  • No local Python or PostgreSQL installation needed (all containerized)

Step 1: Clone the Saleor Platform Repository

Saleor provides a ready-made Docker Compose setup that runs all components:

1
2
git clone https://github.com/saleor/saleor-platform.git
cd saleor-platform

Step 2: Configure Environment Variables

Copy and edit the environment file:

1
cp .env.example .env

Key variables to set:

1
2
3
4
5
6
7
8
POSTGRES_PASSWORD=your_postgres_password
POSTGRES_USER=saleor
POSTGRES_DB=saleor
REDIS_URL=redis://redis:6379/0
JWT_SECRET_KEY=your_jwt_secret_key_minimum_32_chars
ALLOWED_CLIENT_HOSTS=localhost,127.0.0.1,your-store.com
ALLOWED_GRAPHQL_ORIGINS=https://your-store.com
CELERY_BROKER_URL=redis://redis:6379/1

Step 3: Start the Platform

1
docker compose up -d

This starts the following services:

  • saleor-api — Core GraphQL API on port 8000
  • saleor-dashboard — Admin dashboard on port 9000
  • saleor-storefront — Demo storefront on port 3000
  • postgres — PostgreSQL database
  • redis — Redis cache and message broker
  • jaeger — Distributed tracing (optional)

Step 4: Run Database Migrations and Create Admin User

1
2
docker compose exec api python manage.py migrate
docker compose exec api python manage.py createsuperuser

Follow the prompts to create your admin account. Then access the dashboard at http://localhost:9000.

Step 5: Configure Your Store

Log into the dashboard and configure:

  1. Channels — Define your sales channels (web store, mobile app, wholesale)
  2. Warehouses — Set up inventory locations
  3. Payment gateways — Configure Stripe, Adyen, or Braintree
  4. Shipping methods — Define zones, rates, and weight-based rules
  5. Products and collections — Import or create your catalog

Production Docker Compose

For production, modify the compose file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
version: "3.9"

services:
  api:
    image: saleor/saleor:3.20
    ports:
      - "8000:8000"
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_DB: ${POSTGRES_DB}
      DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      REDIS_URL: redis://redis:6379/0
      JWT_SECRET_KEY: ${JWT_SECRET_KEY}
      ALLOWED_CLIENT_HOSTS: ${ALLOWED_CLIENT_HOSTS}
      ALLOWED_GRAPHQL_ORIGINS: ${ALLOWED_GRAPHQL_ORIGINS}
      CELERY_BROKER_URL: redis://redis:6379/1
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  dashboard:
    image: saleor/dashboard:3.20
    ports:
      - "9000:80"
    environment:
      API_URI: https://api.your-store.com/graphql/
      APP_MOUNT_URI: /dashboard/
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - saleor-pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  saleor-pgdata:

Saleor-specific considerations for production:

  • nginxe TLS termination at the reverse proxy level (Nginx, Caddy, or Traefik)
  • Configure ALLOWED_GRAPHQL_ORIGINS strictly for your frontend domains
  • Use Saleor’s webhook system to sync orders with external ERP or accounting systems
  • The GraphQL Playground at /graphql/ is invaluable for testing queries during development

Self-Hosting Guide: Vendure

Prerequisites

  • Node.js 20+ and npm
  • PostgreSQL 15+ or MySQL 8+
  • Redis 7+ (recommended for caching)

Step 1: Install the Vendure CLI

1
npm install -g @vendure/cli

Step 2: Create a New Vendure Project

1
2
vendure create my-shop
cd my-shop

The CLI scaffolds a complete Vendure project with TypeScript configuration, including a vendure-config.ts file that controls database, authentication, payment, and plugin settings.

Step 3: Configure the Database

Edit vendure-config.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { VendureConfig } from '@vendure/core';
import { defaultJobQueuePlugin } from '@vendure/job-queue-plugin';
import {
  DefaultLogger,
  LogLevel,
  defaultConfig,
} from '@vendure/core';

export const config: VendureConfig = {
  apiOptions: {
    port: 3000,
    adminApiPath: 'admin-api',
    shopApiPath: 'shop-api',
    cors: {
      origin: ['https://your-store.com'],
      credentials: true,
    },
  },
  authOptions: {
    tokenMethod: ['bearer', 'cookie'],
    superadminCredentials: {
      identifier: 'superadmin@your-store.com',
      password: 'change_this_immediately',
    },
    cookieOptions: {
      secret: process.env.COOKIE_SECRET || 'change-me',
    },
  },
  dbConnectionOptions: {
    type: 'postgres',
    synchronize: false,
    logging: false,
    host: process.env.DB_HOST || 'localhost',
    port: parseInt(process.env.DB_PORT || '5432', 10),
    username: process.env.DB_USERNAME || 'vendure',
    password: process.env.DB_PASSWORD || 'vendure_pass',
    database: process.env.DB_NAME || 'vendure_shop',
  },
  plugins: [
    defaultJobQueuePlugin,
    // Add payment, search, and email plugins here
  ],
};

Step 4: Set Environment Variables

Create a .env file:

1
2
3
4
5
6
7
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=vendure
DB_PASSWORD=vendure_pass
DB_NAME=vendure_shop
COOKIE_SECRET=your_random_32_char_cookie_secret
VENDURE_SECRET=your_random_32_char_vendure_secret

Create the database:

1
2
3
psql -U postgres -c "CREATE DATABASE vendure_shop;"
psql -U postgres -c "CREATE USER vendure WITH PASSWORD 'vendure_pass';"
psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE vendure_shop TO vendure;"

Step 5: Start the Development Server

1
npm run dev

Vendure automatically runs database migrations on first start and creates the superadmin user from your config. Access the admin UI at http://localhost:3000/admin.

Docker Compose Deployment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
version: "3.8"

services:
  vendure:
    image: node:20-alpine
    working_dir: /app
    command: sh -c "npm ci && npm run build && node dist/server/main.js"
    ports:
      - "3000:3000"
    environment:
      DB_HOST: postgres
      DB_PORT: "5432"
      DB_USERNAME: vendure
      DB_PASSWORD: vendure_pass
      DB_NAME: vendure_shop
      COOKIE_SECRET: ${COOKIE_SECRET}
      VENDURE_SECRET: ${VENDURE_SECRET}
    volumes:
      - .:/app
      - /app/node_modules
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped

  admin-ui:
    image: node:20-alpine
    working_dir: /app/admin-ui
    command: sh -c "npm ci && npm run build"
    build:
      context: .
      dockerfile: admin-ui/Dockerfile
    ports:
      - "80:80"
    depends_on:
      - vendure
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: vendure
      POSTGRES_PASSWORD: vendure_pass
      POSTGRES_DB: vendure_shop
    volumes:
      - vendure-pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U vendure"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    restart: unless-stopped

volumes:
  vendure-pgdata:

Essential Vendure plugins for production:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Stripe payments
npm install @vendure/payments-plugin

# Asset management with S3
npm install @vendure/asset-server-plugin

# Email handling
npm install @vendure/email-plugin

# Full-text search with Elasticsearch
npm install @vendure/elasticsearch-plugin

# Or use the built-in search:
# DefaultSearchPlugin is included in @vendure/core

Vendure B2B Considerations

Vendure shines in B2B scenarios with features that the other platforms handle less elegantly:

  • Customer groups with custom pricing rules
  • Quote management for negotiated pricing
  • Custom fields on any entity (products, orders, customers) without schema migrations
  • Channel-based permissions for multi-tenant storefronts

To add a custom field to the Product entity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { Product, CustomFields } from '@vendure/core';

export const config: VendureConfig = {
  customFields: {
    Product: [
      { name: 'sku', type: 'string', label: [{ languageCode: 'en', value: 'SKU' }] },
      { name: 'manufacturer', type: 'string' },
      { name: 'weight', type: 'float' },
      { name: 'isDigital', type: 'boolean', defaultValue: false },
    ],
  },
};

Performance and Scaling

Medusa Performance

Medusa’s Node.js architecture handles moderate traffic well. For high-traffic stores:

  • Use Redis for session storage and job queue processing
  • Enable CDN caching for product images and static assets
  • Use a reverse proxy (Nginx, Caddy) for SSL termination and load balancing
  • Consider horizontal scaling with multiple Medusa instances behind a load balancer

Typical resource requirements:

  • Small store (< 100 products, < 10 orders/day): 1 CPU, 1GB RAM
  • Medium store (100–1000 products, 10–100 orders/day): 2 CPU, 2GB RAM
  • Large store (1000+ products, 100+ orders/day): 4+ CPU, 4GB+ RAM, separate database server

Saleor Performance

Saleor’s Django-based architecture is robust for high-traffic stores:

  • GraphQL query complexity analysis prevents expensive queries
  • Celery workers handle async tasks (emails, webhooks, reports)
  • Redis caching significantly reduces database load for product queries
  • Jaeger tracing helps identify slow GraphQL resolvers in production

Typical resource requirements:

  • Small store: 2 CPU, 2GB RAM (Django + PostgreSQL + Redis)
  • Medium store: 4 CPU, 4GB RAM, with separate PostgreSQL
  • Large store: 4+ CPU, 8GB+ RAM, separate PostgreSQL and Redis, multiple Celery workers

Vendure Performance

Vendure’s NestJS architecture provides solid performance with TypeScript type safety:

  • Built-in job queue handles background tasks without external dependencies
  • GraphQL DataLoader prevents N+1 query problems
  • MySQL support can be advantageous for teams with existing MySQL infrastructure
  • TypeORM connection pooling handles concurrent requests efficiently

Typical resource requirements:

  • Small store: 1 CPU, 1GB RAM
  • Medium store: 2 CPU, 2GB RAM
  • Large store: 4 CPU, 4GB RAM, separate database

Choosing the Right Platform

Choose Medusa if:

  • You want the closest open-source equivalent to Shopify’s feature set
  • Your team is comfortable with JavaScript/Node.js
  • You value a large plugin ecosystem and community
  • You prefer REST APIs over GraphQL
  • You need rapid prototyping with ready-made storefront starters

Choose Saleor if:

  • GraphQL is your primary API preference
  • You need strong internationalization (multi-currency, multi-language, multi-channel)
  • You want a polished, production-ready admin dashboard
  • Your store serves multiple regions with different pricing and tax rules
  • You have Python expertise on your team

Choose Vendure if:

  • Your team works primarily with TypeScript and NestJS
  • You are building a B2B store with complex pricing, customer groups, or custom fields
  • You need MySQL support alongside PostgreSQL
  • Developer experience and type safety are top priorities
  • You want a clean plugin architecture that does not require modifying core code

Conclusion

All three platforms are production-ready, well-maintained, and genuinely open source. The decision ultimately comes down to your team’s technical stack and your store’s specific requirements.

For a solo developer or small team building a standard B2C store quickly, Medusa offers the gentlest learning curve and the richest plugin ecosystem. For a growing international brand that needs multi-channel sales and GraphQL APIs, Saleor provides the most polished experience. For a B2B-focused operation or a TypeScript-heavy team, Vendure delivers the strongest developer experience and extensibility.

Regardless of which platform you choose, self-hosting your e-commerce backend gives you complete control over your data, your customer relationships, and your technology stack — without paying platform fees or accepting vendor-imposed limitations.

Frequently Asked Questions (FAQ)

Which one should I choose in 2026?

The best choice depends on your specific requirements:

  • For beginners: Start with the simplest option that covers your core use case
  • For production: Choose the solution with the most active community and documentation
  • For teams: Look for collaboration features and user management
  • For privacy: Prefer fully open-source, self-hosted options with no telemetry

Refer to the comparison table above for detailed feature breakdowns.

Can I migrate between these tools?

Most tools support data import/export. Always:

  1. Backup your current data
  2. Test the migration on a staging environment
  3. Check official migration guides in the documentation

Are there free versions available?

All tools in this guide offer free, open-source editions. Some also provide paid plans with additional features, priority support, or managed hosting.

How do I get started?

  1. Review the comparison table to identify your requirements
  2. Visit the official documentation (links provided above)
  3. Start with a Docker Compose setup for easy testing
  4. Join the community forums for troubleshooting