In the global enterprise segment of B2B e-commerce, a common misconception prevails: that an online store for business partners is simply a tweaked version of a B2C retail site with an added field for a Tax ID and a global percentage discount. This illusion quickly shatters when you attempt to digitize actual contractual relationships with large buyers.
Real B2B commerce is defined by extreme variability: every partner buys at individually negotiated rates, has different invoice payment terms, holds varying credit limits, and requires multi-level order approval workflows.
When you try to force these complex rules into traditional monolithic platforms or standard SaaS template solutions, the result is database degradation, lost synchronization with corporate ERP systems, and frustrated buyers.
This technical whitepaper analyzes how we at nolimeo tackle two hard challenges in B2B e-commerce: dynamic personalized pricing (pricing matrix) and asynchronous split payments with credit verification. We use a modular headless architecture and a modern database layer.
1. The Challenge: Query Explosion in Dynamic Price Lists (Pricing Matrix)
In a typical B2C store, a product has one retail price, and perhaps one discount price. In a B2B system, a single product (e.g., an industrial pump) can have hundreds of different prices depending on who is viewing the website. The pricing engine typically combines:
- Basic Retail Price for anonymous visitors.
- Group Discounts (e.g., price lists assigned to categories like "Contractor Tier A", "Wholesale Tier B").
- Individual Contract Prices at the specific company account level (e.g., partner XY has a fixed rate of $1.42 for a specific cable SKU, regardless of any general marketing promotions).
- Volume/Tiered Pricing: price drops based on the quantity added to the cart (e.g., 1-10 pcs = $10/pc, 11-50 pcs = $9/pc, 51+ pcs = $8/pc).
The Problem of Relational Overload (JOIN Explosion)
If a store has 50,000 product variants and 1,500 B2B buyers, each with unique contract terms, a monolithic database quickly becomes a major bottleneck. Every time the catalog is loaded, the application server must run a series of complex and unoptimized queries with multiple JOIN operations across tables for prices, groups, rules, and discounts.
When rendering a category page displaying 24 products, the database calculates hundreds of thousands of relations in real time, paralyzing the server's CPU and driving Time to First Byte (TTFB) past 2 seconds.
The Architectural Solution: Indexed Flat Tables and a Dedicated Pricing Service
In our projects built on Medusa.js and Next.js, we solve this by isolating the pricing logic into a dedicated database structure with high index density. Instead of calculating discounts "on the fly" through complex mathematical algorithms in PHP or SQL, we store pre-calculated net prices directly for combinations of customer_id + SKU + quantity.
Traditional Monolith (Slow synchronous recalculations):
User ──> [ Web Server ] ──> [ SQL JOINs: Discounts, Price Lists, Groups, Taxes, Products ] ──> Slow TTFB (>1.5s)
nolimeo Headless Stack (Fast response):
User ──> [ Next.js Edge ] ──> [ Redis Cache / Pre-calculated Price Index ] ──> Fast page rendering
▲
(Asynchronous write on ERP changes)
│
[ Node.js Middleware (Zod Validation) ] ──> [ Medusa.js PostgreSQL ]
Database Model Implementation in Drizzle ORM
For maximum performance, type-safety, and a clean structure, we use TypeScript and Drizzle ORM. Below is a simplified, production-grade schema that handles both custom prices and tiered volume rates:
// src/db/schema/pricing.ts
import { pgTable, uuid, varchar, integer, timestamp, index, check } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
// B2B Customer Accounts (Companies)
export const b2bCompanies = pgTable("b2b_companies", {
id: uuid("id").primaryKey().defaultRandom(),
companyName: varchar("company_name", { length: 255 }).notNull(),
companyRegNumber: varchar("company_reg_number", { length: 50 }).unique().notNull(),
vatNumber: varchar("vat_number", { length: 50 }),
creditLimit: integer("credit_limit").default(0).notNull(), // Value in cents to eliminate float errors
currentBalance: integer("current_balance").default(0).notNull(), // Currently utilized credit balance
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// Price Lists (can be group-based or assigned to a specific company)
export const b2bPriceLists = pgTable("b2b_price_lists", {
id: uuid("id").primaryKey().defaultRandom(),
name: varchar("name", { length: 255 }).notNull(),
companyId: uuid("company_id").references(() => b2bCompanies.id, { onDelete: "cascade" }),
priority: integer("priority").default(0).notNull(), // Higher number = higher priority during price conflicts
validFrom: timestamp("valid_from"),
validTo: timestamp("valid_to"),
createdAt: timestamp("created_at").defaultNow().notNull(),
}, (table) => ({
companyIdx: index("price_list_company_idx").on(table.companyId),
}));
// Price List Items with Tiered Pricing Support
export const b2bPrices = pgTable("b2b_prices", {
id: uuid("id").primaryKey().defaultRandom(),
priceListId: uuid("price_list_id").references(() => b2bPriceLists.id, { onDelete: "cascade" }).notNull(),
variantSku: varchar("variant_sku", { length: 100 }).notNull(),
priceInCents: integer("price_in_cents").notNull(), // Net price in cents
minQuantity: integer("min_quantity").default(1).notNull(), // Tier threshold (e.g. valid from 10 units)
createdAt: timestamp("created_at").defaultNow().notNull(),
}, (table) => ({
skuListIdx: index("sku_list_idx").on(table.variantSku, table.priceListId),
qtyCheck: check("min_qty_positive", sql`${table.minQuantity} >= 1`),
priceCheck: check("price_positive", sql`${table.priceInCents} >= 0`),
}));
With this data model, our lead developer can write a very fast query that retrieves the best valid price for a specific client and quantity with a single indexed lookup:
-- Query to find the lowest valid B2B price for SKU 'PUMP-INDUSTRIAL-001' with a quantity of 15 pieces
SELECT p.price_in_cents
FROM b2b_prices p
JOIN b2b_price_lists pl ON p.price_list_id = pl.id
WHERE p.variant_sku = 'PUMP-INDUSTRIAL-001'
AND p.min_quantity <= 15
AND (pl.company_id = 'c8b671a5-29e8-466d-8854-bd6b680373ab' OR pl.company_id IS NULL)
AND (pl.valid_from IS NULL OR pl.valid_from <= NOW())
AND (pl.valid_to IS NULL OR pl.valid_to >= NOW())
ORDER BY pl.priority DESC, p.price_in_cents ASC
LIMIT 1;
This database structure moves a large part of the work away from the relational database. By moving to asynchronous price fetching via Next.js Server Components, the visual skeleton of the page loads from the CDN edge network quickly, and custom prices are streamed and injected using React Suspense.
2. The Challenge: Credit Checks and Invoice Split Payments
One of the biggest departures from B2C retail is checkout behavior. B2B buyers rarely pay via card during checkout. The industry standard is invoice payment with terms (e.g., net 14, 30, or 60 days).
For the seller, this introduces credit risk. Every company account is typically assigned a credit limit inside the ERP system (e.g. SAP, NetSuite, QuickBooks, or Microsoft Dynamics), dictating that outstanding invoices cannot exceed $10,000.
If a buyer attempts to place an order that exceeds their available credit limit, the nolimeo system must handle this dynamically:
- Block the order and request payment of outstanding balances.
- Allow a split payment: charge the purchase up to the available credit limit on the invoice, and require the remaining balance to be paid via card or wire transfer through an integrated payment gateway.
Asynchronous Split Payment Architecture
To eliminate outages and ensure integration reliability with corporate ERP systems, we avoid synchronous API calls during checkout. If the ERP system experiences even a temporary connection drop, checkout would fail, showing the buyer an error screen.
Our asynchronous checkout flow operates as follows:
[ Next.js Frontend ] ──( 1. Cart Validation )──> [ Custom Node.js Middleware ]
│
┌───────────────────────────┴───────────────────────────┐
▼ (Valid Data) ▼ (Zod Validation)
[ PostgreSQL Transaction ] [ Verify Limits in Redis ]
│ │
├───────────────────────────────────────────────────────┘
▼
[ Split Amount: Invoice vs. Card ]
│
┌──────────────┴──────────────┐
▼ (Upfront Part) ▼ (Invoice Part)
[ Payment Gateway (Stripe/Adyen) ] [ Credit Reservation in DB ]
│ │
└──────────────┬──────────────┘
▼
[ Redis Queue / BullMQ ] ──( Async Processing )──> [ Corporate ERP ]
Strict B2B Order Validation via Zod
Every incoming checkout request must pass structural integrity checks (Type-Safety) on our middleware to prevent price manipulation and unauthorized credit limit overrides.
Below is an example of our TypeScript schema using Zod:
// src/lib/validation/checkout.ts
import { z } from "zod";
export const B2BCheckoutSchema = z.object({
companyId: z.string().uuid("Invalid company identifier"),
cartId: z.string().uuid("Invalid cart identifier"),
items: z.array(z.object({
sku: z.string().min(1, "SKU cannot be empty"),
quantity: z.number().int().positive("Quantity must be a positive integer"),
unitPriceInCents: z.number().int().nonnegative(),
})).min(1, "Cart cannot be empty"),
paymentStrategy: z.enum(["invoice", "card", "split"]),
splitDetails: z.object({
invoiceAmountInCents: z.number().int().nonnegative().default(0),
cardAmountInCents: z.number().int().nonnegative().default(0),
}).optional(),
shippingAddressId: z.string().uuid("Invalid shipping address identifier"),
});
export type B2BCheckoutInput = z.infer<typeof B2BCheckoutSchema>;
/**
* Validates company credit limits before processing payment
*/
export function validateCreditAllocation(
requestedCredit: number,
allowedLimit: number,
currentBalance: number
): { allowed: boolean; remainingLimit: number } {
const availableCredit = allowedLimit - currentBalance;
if (requestedCredit > availableCredit) {
return {
allowed: false,
remainingLimit: Math.max(0, availableCredit),
};
}
return {
allowed: true,
remainingLimit: availableCredit - requestedCredit,
};
}
3. Resolving Failures and Edge-Cases
In real-world IT systems, failures happen. When designing an enterprise B2B platform, you must plan for third-party systems to go offline. At nolimeo, we build fault-tolerant systems. Let's look at two common failure scenarios in B2B checkout:
Edge-Case #1: ERP Outage During Credit Balance Updates
Problem: The customer completes an order, our system reserves $4,000 from their credit limit, and sends the balance update to the merchant's ERP system. However, the ERP network times out (504 Gateway Timeout) or crashes. If we terminate the transaction, a discrepancy arises: the e-shop displays a different credit balance than the actual accounting records, risking credit over-allocation.
Solution: We use distributed PostgreSQL transactions and asynchronous job queues via Redis (BullMQ) with at-least-once delivery semantics.
On API failure, the order and credit adjustment are saved locally in a pending_erp_sync status. The task is queued and retried with exponential backoff. Until the ERP responds successfully, the system holds the data safely. If the failure persists past 12 hours, the job moves to a Dead Letter Queue (DLQ), and our lead developer receives a high-priority alert on our SLA monitoring channel.
Edge-Case #2: Payment Gateway Timeout During Split Payment Verification
Problem: During a split payment, the user pays a portion of the order via card (Stripe) and the rest goes on the invoice. Stripe charges the card, but during the redirect back to the store, the client's network connection drops (e.g. passing through a tunnel on mobile). The webhook from Stripe fails to arrive in time. The order remains marked as "unpaid" even though the customer was charged and the credit limit was partially reserved.
Solution: Our systems implement asynchronous reconciliation using a unique idempotency_key assigned to every cart session.
When the Stripe webhook finally arrives (or when our cron-based payment check triggers), the system locks the database row (SELECT ... FOR UPDATE), verifies the transaction status directly with the gateway API, verifies the reserved B2B credit, and finishes processing the order asynchronously without requiring administrator intervention.
4. Comparison: Custom Headless (Medusa.js) vs. Traditional B2C Monoliths
Transitioning to headless e-commerce and decoupling frontend and backend layers dramatically affects performance and business stability:
| Parameter | Traditional Monoliths / Templates | Custom Headless (nolimeo Stack) |
|---|---|---|
| B2B Price Response (TTFB) | Slow (1.2s - 2.5s) due to heavy database queries on each page view | Much faster via async streaming (RSC + Redis) |
| Credit Checks & Split Payments | Highly difficult without unstable and costly third-party plugins | Native business logic backed by strict Zod schema validation |
| Legacy ERP Integrations | Synchronous and fragile (ERP outage crashes checkout) | Asynchronous with reliable delivery via Redis queues |
| Price Data Security | Evaluated at the application layer (high leakage risks) | Fully isolated at the database level using PostgreSQL RLS |
| IP Ownership | Vendor lock-in (you don't own the code) | Full and sovereign ownership of your codebase |
Conclusion: Build a Sovereign Foundation for B2B Growth
Moving a mid-market or enterprise company to headless B2B e-commerce is not a cheap or simple step. It requires a detailed analysis of your existing workflows and coordinated effort from senior developers.
However, it is the only way to eliminate technical debt, gain complete ownership of your primary digital sales channel, and deliver a user experience that outperforms the competition.
At the nolimeo technology studio, we focus on engineering and practical solutions. We build functional systems that solve real-world problems. When working with us, you communicate directly with senior developers, avoid bloated agency overhead, and get robust, secure software with long-term support.
Have concerns about the stability of your current B2B setup, or looking for a tech partner to migrate from a monolithic platform without data loss? Contact us and we’ll review your current setup, risks, and migration options.
