@thorprovider/adapters
v4.2.0
Published
Framework-agnostic commerce adapters for Thor Commerce ecosystem - Multi-platform support with unified API
Downloads
520
Readme
title: "@thorprovider/adapters" purpose: "Explain the adapter package, its value proposition, and how to use the unified commerce provider layer" lastReviewed: "2026-05-01"
@thorprovider/adapters v2.0
Unified commerce adapters for Thor Commerce ecosystem
Framework-agnostic TypeScript library providing a unified interface for multiple e-commerce platforms (Medusa, Shopify, WooCommerce, etc.).
🎯 Why This Package Exists
Problem: Direct backend integration leads to:
- ❌ Vendor lock-in (Medusa code in 50+ files → impossible to switch to Shopify)
- ❌ Inconsistent data structures (Medusa Product ≠ Shopify Product ≠ WooCommerce Product)
- ❌ Duplicate business logic (cart logic in 10 apps = 10x maintenance)
- ❌ Framework coupling (Medusa SDK tied to Next.js server actions)
Solution: @thorprovider/adapters provides:
- ✅ Unified Interface: One API for all backends (switch provider with 1 line change)
- ✅ Bidirectional Compatibility Layer: Full read and write support — Backend responses →
@thorprovider/types(inbound) and@thorprovider/types→ Backend API payloads (outbound). Mutations (cart, orders, customers, addresses) are fully abstracted. - ✅ Framework Agnostic: Works with Next.js, Remix, Astro, or vanilla JS
- ✅ Capability Detection: 21 capabilities for adaptive UIs (does backend support X?)
Inspired by: Vercel Commerce (multi-provider), Stripe SDK (unified payments), Auth.js (multi-auth-provider)
💰 ROI / Value Proposition
Backend Migration Cost Reduction
// Without @thorprovider/adapters (direct Medusa integration)
Migration from Medusa → Shopify:
1. Find all Medusa SDK calls: 200+ files
2. Rewrite each call to Shopify API: 40 hours
3. Transform data structures: 20 hours
4. Update tests: 10 hours
5. Fix bugs from migration: 20 hours
= 90 hours per app × $100/hour = $9,000 per app
For 10 apps: $90,000 migration cost
// With @thorprovider/adapters
1. Change provider in config: 1 line
createProvider({ provider: 'shopify' }) // was 'medusa'
2. Verify transforms work: 2 hours
3. Update env variables: 1 hour
= 3 hours × $100/hour = $300 per app
For 10 apps: $3,000 migration cost
// ROI: $90,000 - $3,000 = $87,000 saved (97% cost reduction)Development Speed
// Traditional approach (per feature)
1. Learn Medusa SDK: 4 hours
2. Implement getProducts(): 2 hours
3. Handle errors: 1 hour
4. Add retry logic: 2 hours
5. Write tests: 2 hours
= 11 hours per feature
// With @thorprovider/adapters
1. const products = await commerce.getProducts();
2. Already has: types, errors, retries, tests
= 10 minutes per feature
// Speed improvement: 66x fasterMulti-Tenant Platforms
// Scenario: Platform with 100 clients
// 50 clients use Medusa, 30 use Shopify, 20 use WooCommerce
// Without adapters:
- Maintain 3 separate codebases
- 3x development cost
- 3x bug surface area
// With @thorprovider/adapters:
- Single codebase
- Provider selected per client: clientConfig.provider
- 1x development, 1x maintenance
// ROI: 67% cost reduction for multi-backend platforms🤖 AI Agent Decision Framework
When to Use @thorprovider/adapters
✅ Integrating with e-commerce backends (Medusa, Shopify, custom APIs)
✅ Building multi-tenant platforms (different backends per client)
✅ Need to switch backends in the future (reduce vendor lock-in)
✅ Framework-agnostic apps (Next.js, Remix, Astro, etc.)
✅ Want standardized data shapes across backends
When NOT to Use
❌ Single backend forever + never migrating (direct SDK might be simpler)
❌ Need 100% of platform-specific features (use native SDK)
❌ Non-commerce application (no products/carts/orders)
❌ Backend has no JS SDK (need REST/GraphQL client first)
Decision Tree
Need e-commerce backend integration?
├─ YES → Multiple backends OR might switch?
│ ├─ YES → Use @thorprovider/adapters (future-proof)
│ └─ NO → Single backend forever?
│ ├─ Need framework flexibility → Use @thorprovider/adapters
│ └─ Tight coupling OK → Use native SDK
└─ NO → Use generic HTTP client (fetch/axios)Key Rules for AI Agents
- NEVER import React/Next.js in adapters code (framework-agnostic layer)
- ALWAYS transform to @thorprovider/types (don't expose backend-specific types)
- ALWAYS use try/catch with proper error handling (CommerceError)
- DOCUMENT capabilities when adding new backends (updateCapabilities())
- WRITE transform functions for each data type (transformProduct, transformCart)
Supported Providers
Currently implemented:
- ✅ Mock — Fixture-backed provider for local development, previews, and backend-optional storefront bootstrapping
- ✅ Medusa JS (v2) — Full implementation with multi-tenant support
Planned (Phase 2–3):
- 🟡 Shopify — In progress
- 🟡 BigCommerce — Planned
- 🟡 WooCommerce — Planned
- 🟡 Spree — Planned
- 🟡 Magento — Planned
Provider Selection
// Mock provider (safe default for preview / development)
const mockCommerce = createProvider({
provider: 'mock',
config: {
currencyCode: 'USD',
}
});
// Medusa backend
const commerce = createProvider({
provider: 'medusa',
config: {
baseUrl: process.env.NEXT_PUBLIC_COMMERCE_API_URL,
publishableKey: process.env.NEXT_PUBLIC_COMMERCE_API_KEY,
currencyCode: 'USD',
}
});
const products = await commerce.getProducts();🔒 Multi-Tenant Data Isolation: Sales Channel Filtering
Design Principle: Every product query in @thorprovider/adapters includes explicit sales channel filtering to ensure one storefront instance never sees another tenant's products.
The 1:1 Relationship
V0 Starter Instance (Storefront)
↓
NEXT_PUBLIC_SALES_CHANNEL_ID = "sc_01KJJY1..."
↓
All product queries automatically filter:
{
sales_channel_id: [process.env.NEXT_PUBLIC_SALES_CHANNEL_ID],
...
}
↓
Backend returns products linked to THIS channel ONLY
↓
Result: Multi-tenant isolation at data layerImplementation (Medusa Provider)
All product retrieval methods filter by sales channel (updated 2026-03-26):
// Example: getProducts() in src/providers/medusa/index.ts
async getProducts(options = {}) {
const salesChannelId = process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
const queryParams = {
q: options.query,
limit: options.first ?? 100,
// ... other params
};
// CRITICAL: Apply sales channel filter
if (salesChannelId) {
queryParams.sales_channel_id = [salesChannelId];
}
const { products } = await this.sdk.store.product.list(queryParams);
return products.map(p => transformProduct(p));
}Methods implementing this pattern:
- ✅
getProducts()— Main product listing - ✅
getProduct()— Single product by handle - ✅
getCollectionProducts()— Products in collection - ✅
getCategoryProducts()— Products in category - ✅
createLineItem()— Pre-check variant validation
Why Explicit Filtering?
Question: Medusa's publishable API key already restricts unauthorized endpoints. Why add
sales_channel_idparameter?Answer: Defense-in-depth + Future-proofing
- API Key: Prevents unauthorized endpoint access (✅)
- sales_channel_id parameter: Prevents data leakage within store endpoints (✅)
- Together: Best practice for multi-tenant SaaS platforms
Configuration (Per Platform)
// Medusa (REQUIRED for multi-tenant)
const commerce = createProvider({
provider: 'medusa',
config: {
baseUrl: process.env.NEXT_PUBLIC_COMMERCE_API_URL,
publishableKey: process.env.NEXT_PUBLIC_COMMERCE_API_KEY,
currencyCode: 'USD',
// CRITICAL: Derived from env, used in every product query
// salesChannelId: process.env.NEXT_PUBLIC_SALES_CHANNEL_ID,
}
});
// Shopify (when implemented — optional parameter, uses default if not set)
// NEXT_PUBLIC_SHOPIFY_STOREFRONT_ID = '...'
// WooCommerce (optional — uses default site)
// NEXT_PUBLIC_WOOCOMMERCE_SITE_ID = '1'Learn more: packages/core/docs/storefront-sales-channel-mapping.md for platform-specific implementation details and multi-brand deployment patterns.
Adding a New Provider
To implement Shopify, BigCommerce, or another platform:
- Read the standardized guide: .development-guides/add-new-provider.md
- Follow 11-step checklist: Registration in L1, implementation in L2, factory updates, tests, docs
- Use transform pattern: Convert platform-specific types to
@thorprovider/types
Example checklist item:
- [ ] Update
SupportedProviderTypeenum inpackages/types/src/provider.ts - [ ] Create
packages/adapters/src/providers/[platform]/index.ts - [ ] Implement
CommerceProviderinterface - [ ] Add discriminated union case to factory
- [ ] Write tests
- [ ] Update documentation
Transform Function Pattern
// ALWAYS follow this pattern when adding new providers
export function transformProduct(backendProduct: any): Product {
return {
id: backendProduct.id,
handle: backendProduct.slug,
title: backendProduct.name,
// ... transform all fields to @thorprovider/types shape
};
}Imbricación Principle: No Downstream Transformations
@thorprovider/adapters is the ONLY transformation layer in the Thor Stack architecture.
Backend (Medusa/Shopify) → Adapter.transform() → @thorprovider/types → Elements → Starters
↑ ONLY transformation pointKey Rules:
- Adapters transform ONCE: Backend-specific format →
@thorprovider/typesunified format - Elements consume directly: Import types from
@thorprovider/types, never create local copies - Starters pass through: Use
@thorprovider/typesdirectly, no UI-specific transformations - Single source of truth:
@thorprovider/typesdefines the contract for entire system
Why this matters:
- Changes to
@thorprovider/typespropagate instantly (no intermediate layers to update) - Zero duplicate type definitions (DRY principle)
- 78× faster development (1 change vs 78 files)
Example: Order Management
// ✅ CORRECT: Adapter transforms backend → @thorprovider/types
// File: src/providers/medusa/transforms.ts
export function transformOrder(medusaOrder: any): Order {
return {
id: medusaOrder.id,
orderNumber: medusaOrder.display_id,
status: transformOrderStatus(medusaOrder.status),
createdAt: medusaOrder.created_at,
items: medusaOrder.items.map(transformOrderItem),
total: { amount: medusaOrder.total.toString(), currencyCode: 'USD' },
// ... all fields mapped to @thorprovider/types Order
}
}
// ✅ CORRECT: Elements import from @thorprovider/types
// File: packages/components/src/patterns/modules/profile/ProfileModule.props.ts
import type { Order } from '@thorprovider/types'
export interface ProfileModuleProps {
orders?: Order[] // Uses @thorprovider/types directly
}
// ✅ CORRECT: Starters pass through
// File: packages/core/app/[locale]/profile/page.tsx
const orders = await commerce.getOrders(customerId)
<ProfileModule orders={orders} /> // No transformation!
// ❌ WRONG: Creating intermediate types in L5
interface UIOrder { // ← Duplicates @thorprovider/types Order
id: string
date: string // ← Different field name (vs createdAt)
total: number // ← Different structure (vs Money object)
}
function transformOrderForUI(order: Order): UIOrder { ... } // ← Unnecessary layerReference: See commits c3d5efd2 and cbaa83ef for real-world refactoring that eliminated an unnecessary transformer by making ProfileModule use @thorprovider/types directly.
Quick Start
yarn add @thorprovider/adaptersimport { createProvider } from '@thorprovider/adapters';
const commerce = createProvider({
provider: 'medusa',
config: {
baseUrl: process.env.NEXT_PUBLIC_COMMERCE_API_URL!,
publishableKey: process.env.NEXT_PUBLIC_COMMERCE_API_KEY!,
},
});
// Use unified API
const products = await commerce.getProducts();
const cart = await commerce.createCart();Features
✅ Multi-platform - Single API for Medusa, Shopify, WooCommerce, etc.
✅ Type-safe - Full TypeScript support with strict types
✅ Framework agnostic - Works with Next.js, Remix, Astro, or any JS framework
✅ Easy swapping - Change backends with minimal code changes
✅ Capability Detection - 21 backend capabilities for adaptive UIs (NEW in v2.0)
✅ Authentication - Built-in JWT and session-based auth
✅ Auto-refresh - Automatic token refresh for JWT
✅ Profile Management - Complete customer operations with capability awareness
Core Concepts
Unified Interface
All providers implement the same interface:
interface CommerceProvider {
readonly name: string;
readonly version: string;
readonly capabilities: BackendCapabilities; // NEW!
// Products
getProduct(handle: string): Promise<Product | undefined>;
getProducts(options?: GetProductsOptions): Promise<Product[]>;
// Collections
getCollection(handle: string): Promise<Collection | undefined>;
getCollections(): Promise<Collection[]>;
// Cart
createCart(): Promise<Cart>;
addToCart(cartId: string, lines: CartLineInput[]): Promise<Cart>;
// Auth (optional)
auth?: AuthService;
}Capabilities System (v2.0)
Check backend support before attempting operations:
// Check if backend supports email updates
if (commerce.capabilities.updateEmail) {
// Show email field as editable
} else {
// Show as disabled with info message
}21 Capabilities across 5 categories:
- Profile: updateProfile, updateEmail, changePassword, deleteAccount, etc.
- Avatar: uploadAvatar, avatarViaMetadata
- Cart: promoCode, giftCards, notes, customAttributes
- Products: variants, categories, inventory, digital, reviews
- Auth: JWT, session, OAuth, passwordless
See: Capabilities System
Supported Platforms
| Platform | Status | Documentation | |----------|--------|---------------| | Medusa v2 | ✅ Production Ready | Medusa Docs | | Shopify | 🚧 Planned | Shopify API | | WooCommerce | 🗓️ Roadmap | WooCommerce API | | BigCommerce | 🗓️ Roadmap | BigCommerce API | | Local (Mock) | ✅ Production Ready | For testing, previews, and backend-optional storefronts |
See: Providers Overview
Documentation
Getting Started
- Installation & Quick Start
- Services Overview - Available operations
- Providers Overview - Supported platforms
Key Features
- Capabilities System - Feature detection (NEW!)
- Authentication - JWT and session auth
- Profile Management - Customer operations
- Category Customization & Inheritance - Personalize inherited categories (NEW!)
Advanced
- Add New Provider - Step-by-step workflow
- Add New Service - Service workflow
- Type Definitions - Full TypeScript types
Examples
Next.js Integration
// lib/commerce.ts
import { createProvider } from '@thorprovider/adapters';
export const commerce = createProvider({
provider: 'medusa',
config: {
baseUrl: process.env.NEXT_PUBLIC_COMMERCE_API_URL!,
publishableKey: process.env.NEXT_PUBLIC_COMMERCE_API_KEY!,
auth: {
method: 'jwt',
storage: 'localStorage',
},
},
});
// app/products/page.tsx
import { commerce } from '@/lib/commerce';
export default async function ProductsPage() {
const products = await commerce.getProducts();
return <ProductList products={products} />;
}Authentication
// Login
const { customer, token } = await commerce.auth.login({
email: '[email protected]',
password: 'password123',
});
// Register
const { customer } = await commerce.auth.register({
email: '[email protected]',
password: 'password123',
first_name: 'John',
last_name: 'Doe',
});
// Get current customer
const customer = await commerce.auth.getCurrentCustomer();
// Update profile (capability-aware)
if (commerce.capabilities.updateProfile) {
await commerce.auth.updateCustomer({
first_name: 'Jane',
phone: '+1234567890',
});
}Cart Management
// Create cart
const cart = await commerce.createCart();
// Add items
const updated = await commerce.addToCart(cart.id!, [
{ merchandiseId: 'variant_123', quantity: 2 },
]);
// Update quantities
await commerce.updateCart(cart.id!, [
{ id: 'line_1', quantity: 3 },
]);
// Remove items
await commerce.removeFromCart(cart.id!, ['line_2']);Switching Providers
Change platforms with minimal code changes:
// From Medusa
const commerce = createProvider({
provider: 'medusa',
config: { /* ... */ }
});
// To Shopify (when available)
const commerce = createProvider({
provider: 'shopify',
config: { /* ... */ }
});
// ✅ Rest of your code stays the same!
const products = await commerce.getProducts();Architecture
@thorprovider/adapters/
├── docs/ # Modular documentation
│ ├── getting-started.md
│ ├── services-overview.md
│ ├── providers-overview.md
│ ├── capabilities-system.md
│ ├── authentication.md
│ └── profile-management.md
├── types/ # Unified types (Product, Cart, etc.)
├── providers/ # Provider implementations
│ ├── medusa/ # Medusa v2 provider
│ └── shopify/ # (coming soon)
├── core/ # Core utilities (errors, logger)
└── factory/ # Provider factoryDesign Principles
- Provider Pattern - One adapter per platform
- Interface First - Unified API defined by shared types
- Framework Agnostic - Zero dependencies on React/Next.js
- TypeScript Strict - Strong types for all operations
- Composable - Providers can be easily swapped
- Capability-Aware - Build adaptive UIs that work across backends
Contributing
Contributions welcome! See CONTRIBUTING.md.
Adding a Provider
- Define capabilities in
capabilities.ts - Implement API client
- Implement core services (Products, Cart, Auth)
- Map responses to unified types
- Write integration tests
- Update factory and documentation
Version
Current: v2.0 - Production Ready ✅
New in v2.0:
- ✅ Capabilities system (21 backend capabilities)
- ✅ Profile management with capability awareness
- ✅ Auto token refresh
- ✅ Comprehensive TypeScript types
- ✅ Modular documentation
See: CHANGELOG.md
Resources
- Medusa Documentation
- TypeScript Handbook
- Examples - Code samples
- Thor Commerce - Monorepo overview
License
MIT
