Imagine needing to implement a complex discount system for wholesalers, integrate with 1C, and ensure operation in 10 countries. Off-the-shelf SaaS solutions won't work—licenses are expensive (e.g., commercetools charges a significant fee based on turnover), and customization is limited by their API. Our team, with 10 years of e-commerce experience and 5 years specializing in Vendure, has completed over 15 Vendure projects with a 100% client satisfaction rate. We offer Vendure—an open-source framework built on NestJS and TypeScript. It gives full control: from the database schema to GraphQL resolvers. Compared to SaaS, licensing cost savings can reach 70% for turnovers from $500k per year. In one project, we replaced commercetools with Vendure for a large B2B store. The result was $15k monthly savings and the ability to implement custom tax calculation logic without workarounds.
How to Set Up Vendure in 5 Steps
- Install dependencies and configure your database (PostgreSQL recommended).
- Create custom plugins for business logic (taxes, shipping, payments).
- Run database migrations to sync the schema.
- Start the server and worker processes.
- Connect your storefront via GraphQL.
What Problems Do We Solve?
Multitenancy. When you need to run multiple stores on a single core (different regions, brands), Vendure offers the Channels mechanism. Each channel isolates catalog, prices, currency, taxes, and payment methods. One instance easily handles dozens of stores with separate entities. Switching is done via the vendure-token header in Shop API requests. A common rookie mistake is omitting the token, causing the request to be processed in the default channel and leading to confusion. Vendure official documentation states that Channels allow separate catalogs per store.
Flexible Tax and Shipping Logic. Standard solutions often fall short. Vendure allows overriding TaxCalculationStrategy and ShippingCalculator via plugins. We implemented custom tax calculation for goods with varying rates depending on region and buyer type.
Payment Integration. Stripe is supported out of the box. Custom handlers can connect any system with an API. We built a YooKassa integration with full lifecycle: payment creation, confirmation, refund. The handler code is in the section below.
How We Do It: Configuration and Custom Plugins
The standard project structure includes a plugins directory for custom modules, email handlers, and payment handlers. Example configuration:
// src/vendure-config.ts
import { VendureConfig } from "@vendure/core";
import { defaultEmailHandlers, EmailPlugin } from "@vendure/email-plugin";
import { AssetServerPlugin } from "@vendure/asset-server-plugin";
import { AdminUiPlugin } from "@vendure/admin-ui-plugin";
export const config: VendureConfig = {
apiOptions: {
port: 3000,
adminApiPath: "admin-api",
shopApiPath: "shop-api",
adminApiPlayground: process.env.NODE_ENV === "development",
},
authOptions: {
tokenMethod: ["bearer", "cookie"],
superadminCredentials: {
identifier: process.env.SUPERADMIN_USERNAME!,
password: process.env.SUPERADMIN_PASSWORD!,
},
cookieOptions: {
secret: process.env.COOKIE_SECRET!,
},
},
dbConnectionOptions: {
type: "postgres",
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
database: process.env.DB_NAME,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
synchronize: false,
migrations: ["dist/migrations/*.js"],
},
paymentOptions: {
paymentMethodHandlers: [stripePaymentHandler, yookassaPaymentHandler],
},
taxOptions: {
taxCalculationStrategy: new CustomTaxCalculationStrategy(),
},
shippingOptions: {
shippingCalculators: [defaultShippingCalculator, tieredShippingCalculator],
shippingEligibilityCheckers: [defaultShippingEligibilityChecker],
fulfillmentHandlers: [manualFulfillmentHandler],
},
plugins: [
AssetServerPlugin.init({
route: "assets",
assetUploadDir: path.join(__dirname, "../static/assets"),
}),
EmailPlugin.init({
devMode: process.env.NODE_ENV === "development",
handlers: defaultEmailHandlers,
templatePath: path.join(__dirname, "../email/templates"),
transport: {
type: "smtp",
host: process.env.SMTP_HOST!,
port: 587,
auth: {
user: process.env.SMTP_USER!,
pass: process.env.SMTP_PASS!,
},
},
}),
AdminUiPlugin.init({
route: "admin",
port: 3002,
}),
LoyaltyPlugin,
B2bPricingPlugin,
ErpSyncPlugin,
],
};
Checkout Flow via Shop API
Interacting with the cart and placing orders is done via GraphQL. Example sequence:
# 1. Add item to order
mutation AddToOrder($productVariantId: ID!, $quantity: Int!) {
addItemToOrder(productVariantId: $productVariantId, quantity: $quantity) {
... on Order {
id
code
totalWithTax
lines {
id
quantity
productVariant { name sku }
unitPriceWithTax
}
}
... on ErrorResult {
errorCode
message
}
}
}
# 2. Set shipping address
mutation SetShippingAddress($input: CreateAddressInput!) {
setOrderShippingAddress(input: $input) {
... on Order { id shippingAddress { fullName streetLine1 city } }
... on NoActiveOrderError { errorCode message }
}
}
# 3. Get shipping methods and select
query GetShippingMethods {
eligibleShippingMethods {
id
name
price
priceWithTax
description
}
}
mutation SetShippingMethod($id: [ID!]!) {
setOrderShippingMethod(shippingMethodId: $id) {
... on Order { id shipping shippingWithTax }
}
}
Payment Integration (YooKassa)
Example handler for YooKassa:
// src/payment-handlers/yookassa.handler.ts
import { CreatePaymentResult, PaymentMethodHandler, LanguageCode } from "@vendure/core";
export const yookassaPaymentHandler = new PaymentMethodHandler({
code: "yookassa",
description: [{ languageCode: LanguageCode.ru, value: "YooKassa" }],
args: {
shopId: { type: "string" },
secretKey: { type: "string", ui: { component: "password-form-input" } },
},
async createPayment(ctx, order, amount, args, metadata): Promise<CreatePaymentResult> {
const yookassa = new YooKassa({
shopId: args.shopId,
secretKey: args.secretKey,
});
const payment = await yookassa.createPayment({
amount: {
value: (amount / 100).toFixed(2),
currency: order.currencyCode,
},
capture: true,
confirmation: {
type: "redirect",
return_url: `${process.env.SHOP_URL}/checkout/confirmation`,
},
description: `Order #${order.code}`,
metadata: { vendure_order_id: order.id },
});
return {
amount,
state: "Authorized",
transactionId: payment.id,
metadata: { confirmationUrl: payment.confirmation.confirmation_url },
};
},
async settlePayment(ctx, order, payment, args) {
return { success: true };
},
async refundPayment(ctx, order, payment, args, lines, adjustment) {
const yookassa = new YooKassa({ shopId: args.shopId, secretKey: args.secretKey });
const refund = await yookassa.createRefund(payment.transactionId, {
amount: { value: (adjustment / 100).toFixed(2), currency: order.currencyCode },
});
return { state: "Settled", transactionId: refund.id };
},
};
Performance and Scaling
Vendure supports Worker/Server separation: heavy tasks (email, export, indexing) are handled in a separate Worker process via Bull. For production, you need at least 2 server instances (load balanced), 1+ worker, and Redis for queues. This approach ensures stability during peak loads—for example, Black Friday.
Why Vendure Is More Cost-Effective Than SaaS?
SaaS platforms like commercetools are convenient, but monthly subscriptions grow with turnover. Vendure is a one-time investment in development and hosting. With significant turnover, licensing cost savings can reach 70%. Additionally, you are not locked into a vendor—code and data are always yours. In terms of business logic customization speed, Vendure outperforms typical SaaS solutions by 3-4 times, as it doesn't require workarounds when working with APIs. For high-volume stores, Vendure is 5 times more cost-effective.
| Characteristic | Vendure | Commercetools |
|---|---|---|
| Model | Self-hosted (open-source) | SaaS |
| Cost | Free (license) | Significantly more expensive |
| Customization | Full (any logic via plugins) | Limited by API |
| Data | Your server | Vendor cloud |
Common Pitfalls When Starting
- Database synchronization enabled in production –
synchronize: truecan overwrite data. Always use migrations. - Queue not configured for Worker – if you don't set up Redis and run the Worker, email and background tasks won't execute.
- Incorrect
vendure-tokenheader – for multitenancy, you must pass the channel token; otherwise, the request will fail with an error.
Development Stages and Timeline
| Stage | Duration |
|---|---|
| Setup, configuration, database, migrations | 2–3 days |
| Catalog import (Products, Variants, Assets) | 3–7 days |
| Custom plugins (taxes, shipping, promo codes) | 5–10 days |
| Storefront (Next.js + GraphQL) | 10–20 days |
| Payment integrations (2–3 providers) | 4–6 days |
| Admin UI customization | 2–4 days |
| Total | 26–50 days |
What Is Included
- Full documentation: deployment guide, configuration README, API specification in GraphQL Playground.
- Access: dedicated server (or cloud recommendations), code repository, admin panel.
- Team training: workshop on Vendure administration and Channels.
- Support: 2 weeks after release—bug fixes, help with deployment updates.
- Our developers hold certifications in NestJS and GraphQL.
- We provide a 14-day post-launch guarantee on bug fixes.
- We offer a satisfaction guarantee: if the project is not delivered on time, we provide free support extensions.
We have extensive experience implementing Vendure and dozens of successful projects—from clothing stores to B2B portals with thousands of SKUs. Our expertise in TypeScript commerce with Vendure ensures robust solutions. For e-commerce development, we leverage Vendure's architecture to deliver custom, scalable stores. Get a consultation: we will assess your project and propose a turnkey architecture. Contact us for a preliminary cost and timeline estimate.
More about Vendure features can be found in the official documentation on GitHub.







