A fork of @payloadcms/plugin-ecommerce with custom enhancements.
# From GitHub, pinned to a specific tag
pnpm add github:marsender/payload-plugin-ecommerce#v3.79.0import { buildConfig } from 'payload'
import { ecommercePlugin } from '@marsender/payload-plugin-ecommerce'
import { stripeAdapter } from '@marsender/payload-plugin-ecommerce/payments/stripe'
export default buildConfig({
plugins: [
ecommercePlugin({
customers: {
slug: 'users',
},
payments: {
paymentMethods: [
stripeAdapter({
stripeSecretKey: process.env.STRIPE_SECRET_KEY!,
}),
],
},
currencies: {
supportedCurrencies: ['USD', 'EUR'],
defaultCurrency: 'USD',
},
}),
],
})- Products & Variants management
- Shopping Cart with server-side operations
- Orders & Transactions
- Address management
- Stripe payment integration for multi-tenant
- Stripe Connect support (multi-vendor/marketplace payments)
- Multi-currency support
- Guest cart support with automatic merge on login
- Cart item operations via REST endpoints
- Multi-tenant cart support (tenant isolation with guest access)
This plugin supports Stripe Connect for marketplace/multi-vendor scenarios where payments need to be routed to different connected accounts (e.g., different coaches, sellers, or vendors).
- Enable Stripe Connect on your platform Stripe account via the Stripe Dashboard
- Each vendor/coach creates a Connected Account and you store their
connected_account_id - Configure the
resolveConnectedAccountcallback in your stripeAdapter:
import { ecommercePlugin } from '@marsender/payload-plugin-ecommerce'
import { stripeAdapter } from '@marsender/payload-plugin-ecommerce/payments/stripe'
export default buildConfig({
plugins: [
ecommercePlugin({
customers: { slug: 'users' },
payments: {
paymentMethods: [
stripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY!,
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET!,
// Resolve the connected account from cart items
resolveConnectedAccount: async ({ cart, req }) => {
const firstItem = cart.items?.[0]
if (!firstItem) return undefined
const productId = typeof firstItem.product === 'object' ? firstItem.product.id : firstItem.product
const product = await req.payload.findByID({
collection: 'products',
id: productId,
depth: 1,
})
// Return the coach's/vendor's Stripe Connect account ID
const coach = product.coach
if (!coach || typeof coach !== 'object') return undefined
return coach.stripeConnectAccountId || undefined
},
}),
],
},
}),
],
})- When
resolveConnectedAccountis provided and returns a connected account ID, the PaymentIntent is created withtransfer_data.destinationset to that account - The platform account processes the payment and automatically transfers funds to the connected account
- The
connectedAccountIdis stored in the transaction record for reference - If no connected account is resolved, the payment goes to the platform account as usual
The plugin provides server-side cart operations via REST endpoints for reliable cart manipulation:
POST /api/carts/:id/add-item- Add an item to the cartPOST /api/carts/:id/update-item- Update item quantity (supports{ $inc: n }for increment/decrement)POST /api/carts/:id/remove-item- Remove an item from the cartPOST /api/carts/:id/clear- Clear all items from the cartPOST /api/carts/:id/merge- Merge a guest cart into an authenticated user's cart
You can also use the cart operations directly in your server-side code:
import { addItem, removeItem, updateItem, clearCart, mergeCart } from '@marsender/payload-plugin-ecommerce'
// Add an item
await addItem({
payload,
cartsSlug: 'carts',
cartID: 'cart-123',
item: { product: 'product-id', variant: 'variant-id' },
quantity: 2,
})
// Update quantity (increment by 1)
await updateItem({
payload,
cartsSlug: 'carts',
cartID: 'cart-123',
itemID: 'item-row-id',
quantity: { $inc: 1 },
})
// Merge guest cart into user cart
await mergeCart({
payload,
cartsSlug: 'carts',
targetCartID: 'user-cart-123',
sourceCartID: 'guest-cart-456',
sourceSecret: 'guest-cart-secret',
})You can provide a custom cartItemMatcher function to define when cart items should be considered the same (and have their quantities combined):
ecommercePlugin({
carts: {
cartItemMatcher: ({ existingItem, newItem }) => {
// Match by product, variant, AND custom delivery option
const productMatch = existingItem.product === newItem.product
const variantMatch = existingItem.variant === newItem.variant
const deliveryMatch = existingItem.deliveryOption === newItem.deliveryOption
return productMatch && variantMatch && deliveryMatch
},
},
})For multi-tenant applications, you can enable tenant isolation for carts while still supporting guest cart access:
ecommercePlugin({
carts: {
allowGuestCarts: true,
multiTenant: {
enabled: true,
tenantsSlug: 'tenants', // default
},
},
})Important: When using multi-tenant carts, do NOT add carts to the @payloadcms/plugin-multi-tenant collections list. The ecommerce plugin handles tenant isolation internally to support both guest and tenant-scoped access.
- Tenant field: A
tenantrelationship field is added to the carts collection (required) - Auto-population: The
populateTenanthook automatically sets the tenant frompayload-tenantorpayload-tenant-domaincookies on cart creation. If no valid tenant can be determined, cart creation fails with an error. - Access control: The
hasTenantAccessfunction provides tenant-scoped access for admins:- Super-admins: Full access to all carts
- Tenant-admins: See carts only from their tenant(s)
- Regular users: Access only their own carts (via
isDocumentOwner) - Guests: Access via secret token (unchanged behavior)
| User Type | Has Secret | Result |
|---|---|---|
| Guest (no auth) | Yes | Access via secret match |
| Guest (no auth) | No | No access |
| Authenticated | Any | Access own carts (customer = user.id) |
| Tenant Admin | Any | All carts for their tenant(s) |
| Super Admin | Any | All carts (full access) |
This fork includes the following enhancements:
-
refreshUser()function in EcommerceProvider: Exposes arefreshUser()function viauseEcommerce()hook that allows the app to refresh the user state on demand. Call this after login/logout to sync the EcommerceProvider with the current auth state, enabling proper client-side navigation without full page reloads. -
deleteAddress()function in EcommerceProvider: Exposes adeleteAddress(addressID)function viauseEcommerce()anduseAddresses()hooks. This allows deleting addresses directly through the provider, which automatically refreshes the addresses list after deletion. -
Simplified address
customerfield auto-assignment: ThebeforeChangehook on the addresses collection now automatically sets thecustomerfield to the current user's ID if not already provided. This works for all authenticated users (customers, admins, or users with multiple roles). Admins can still override by explicitly providing acustomerID when creating an address for another user. -
Stripe Connect support: Added
resolveConnectedAccountoption tostripeAdapter()that enables routing payments to different Stripe Connected Accounts. This is useful for marketplace/multi-vendor scenarios where each seller or coach has their own Stripe account. The connected account ID is stored in the transaction record for reference. -
Stripe SDK v20 (Clover): Upgraded to Stripe Node.js SDK v20 with API version
2025-09-30.clover. This version uses Stripe's new biannual release train versioning. See Stripe's versioning policy for details. -
Multi-tenant cart support: Added
multiTenantoption to carts configuration. When enabled, carts have atenantfield that is auto-populated from cookies, and admin access is scoped by tenant. This allows tenant isolation in the admin panel while still supporting guest cart access via secret tokens. Use this instead of adding carts to the multi-tenant plugin's collections list.
Synchronized with PayloadCMS plugin-ecommerce v3.71.1:
- Server-side cart operations: Cart operations (addItem, removeItem, updateItem, clearCart) now use server-side endpoints for improved reliability and consistency
- Cart merge endpoint: New
/mergeendpoint for merging guest carts into authenticated user carts on login - Custom cart item matcher: Configure
cartItemMatcherto define custom item matching logic variantTypesSlugparameter: Added tocreateVariantsCollectionfor custom variant type collection slugs- New React provider functions:
clearSession,mergeCart,onLogin,onLogout,refreshCart,config,user - Type improvements: New types
CartItemMatcher,CartItemMatcherArgs,EcommerceConfig,SanitizedAccessConfig
Changes are made directly in the plugin source and consumed by pinning a GitHub tag in dependent projects. Tags follow the PayloadCMS version they are compatible with (e.g. v3.79.0).
cd /opt/git/marsender/payload-plugin-ecommerce
# Remove stale node_modules and lockfile before reinstalling (required when upgrading payload version)
pnpm store prune
rm -rf node_modules && rm pnpm-lock.yaml
pnpm install
# Optionally upgrade peer dependencies to a new PayloadCMS version
pnpm update payload@<version> @payloadcms/ui@<version> @payloadcms/translations@<version>
# Update version in package.json to match the target PayloadCMS version, then:
pnpm type-check && pnpm build
git add .
git commit -m "<message reflecting the changes>"
git pushgit tag v<target-version>
git push origin v<target-version>In the dependent project, update package.json to pin the new tag:
"@marsender/payload-plugin-ecommerce": "github:marsender/payload-plugin-ecommerce#v<target-version>"Then reinstall and verify:
pnpm install
pnpm dedupe
pnpm type-checkContributions are welcome! Please open an issue or submit a pull request.
MIT - See LICENSE.md
Based on the official PayloadCMS Ecommerce Plugin.