Why Rented E-commerce Platforms Hold Back B2B Businesses: When to Build a Custom System

By nolimeo · February 17, 2026
banner image

Medium-sized and growing B2B companies often begin their e-commerce journey on rented, out-of-the-box SaaS solutions. This is a rational decision for the early stages of business: low capital expenditures (CAPEX), launching in weeks, and zero immediate need to manage an internal programming team.

The problem arises when the annual digital sales surpass $1 million, the catalog expands to tens of thousands of items, and the business requires deep integration with internal systems. At that stage, the monolithic nature of rented platforms begins to collide with its physical and logical limits.

This technical analysis details three critical bottlenecks of traditional SaaS platforms and defines the transition to a Headless Architecture as a way to eliminate technical debt and gain much more control over your business model.


The Tipping Point: 3 Technical Limits Paralyzing Growing B2B Businesses

When you run complex B2B operations, your sales reality differs diametrically from classic B2C retail. Every client may have custom contractual terms, approval hierarchies, and logistics arrangements. Traditional SaaS platforms, designed primarily for unified B2C checkout, attempt to bridge these differences by layering external applications and plugins. However, this builds an unstable software ecosystem.

1. Rigid B2B Logic and Cart Restrictions

The most common issue with rented solutions is their inability to adapt the checkout flow to individual buyer requirements. A B2B client typically expects:

  • Split Payments: The ability to pay a portion of an invoice upfront as a deposit and the remainder on 14/30-day net terms aligned with a pre-approved credit limit.
  • Multi-Address Delivery on a Single Order: Distributing goods to different branches or project sites directly from a single shopping cart.
  • Approval Hierarchies: A buyer in a client firm can build the cart, but the order must be authorized by their finance manager directly within the client portal before submission.

If you attempt to implement these flows in a rented e-shop, you run into closed source code. You cannot modify the database schema of the cart, nor can you rewrite the native validation rules of the backend. You are forced to use generic workarounds that force your clients into inefficient manual steps.

2. Performance Collapse (TTFB and LCP) Under Large Catalogs and Dynamic Pricing

E-shop speed correlates directly with conversion rates and organic search visibility (SEO). Google penalizes sites with slow Time to First Byte (TTFB) and Largest Contentful Paint (LCP).

Monolithic systems store all data in a single relational database. When loading a product page, the system must run complex database queries (SQL JOINs) across tables of products, variants, inventory levels, attributes, and most importantly—custom price lists. If an e-shop has 20,000 items and 500 B2B customers, each with a unique contract discount on specific categories, the database engine must compute millions of combinations upon every page load.

Monolithic Architecture:
Client ──> [ Web Server / Monolithic Backend ] ──> [ Huge Relational Database ]
             (Price calculations, HTML rendering, SQL JOINs on a single server)
             *Result: TTFB becomes significantly slower under load*

This leads to dramatic page load slowdowns, often stretching into several seconds. In a Next.js headless architecture, we resolve this by decoupling the presentation layer and leveraging Edge Rendering plus dedicated cache layers such as Redis to improve response times even when price matrix complexity grows.

3. Hidden Transaction Fees and Zero IP Asset Value

With rented platforms, you pay not only a flat monthly fee but also a percentage fee on every transaction (often 0.5% to 2%). For a company with an annual e-commerce volume of $5,000,000, this translates to tens of thousands of dollars paid annually just to lease infrastructure.

The largest risk, however, is the complete lack of code ownership (vendor lock-in). All investments in e-shop adjustments, integrations, and customizations are bound to the third-party platform. If that provider decides to raise prices, restrict API limits, or discontinue support for a module, your business has no recourse. You cannot export the database and code to migrate to another host because you do not own the software.


Architectural Solution: Headless & Composable Commerce

Our tech studio, nolimeo, builds modern B2B systems on the principles of Composable Commerce. Under this approach, the e-shop is not a single monolith, but an assembly of specialized modules connected via APIs.

nolimeo Headless Stack:
[ Next.js Frontend (Vercel Edge) ]
        │ (Lightning-Fast API Calls)
        ├───────────────────────────┬───────────────────────────┐
        ▼                           ▼                           ▼
[ Medusa.js Engine ]      [ Custom Node.js Middleware ]   [ Payload CMS ]
 (Orders, Carts)           (ERP Connection, Front/Back)  (Content, Blog, SEO)
  1. Frontend: We use Next.js with React Server Components, deployed on a global CDN (Edge rendering). Pages are statically pre-generated, and dynamic data (such as personalized B2B pricing and stock levels) is loaded asynchronously via ultra-fast API microservices.
  2. Backend: As the engine for business logic, we deploy modern open-source frameworks (such as Medusa.js) built on Node.js and PostgreSQL. The entire backend is under our team's control and is directly customizable to your operations.
  3. Content Management: To manage marketing content, documentation, and product manuals, we integrate a robust Payload CMS accessed via secure API endpoints.

B2B Data Model Example: Flexible Price Mapping in Drizzle ORM

For B2B pricing, we design schemas that map specific prices directly to customer groups or corporate accounts, with support for volume discounts (tiered pricing). Below is a schema representation in TypeScript using Drizzle ORM:

// src/db/schema.ts
import { pgTable, uuid, varchar, integer, timestamp, foreignKey, index } from "drizzle-orm/pg-core";

// Customer groups (e.g., VIP, Wholesale, Hardware Stores)
export const customerGroups = pgTable("customer_groups", {
  id: uuid("id").primaryKey().defaultRandom(),
  name: varchar("name", { length: 255 }).notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

// B2B Customers linked to their company and price list group
export const customers = pgTable("customers", {
  id: uuid("id").primaryKey().defaultRandom(),
  companyName: varchar("company_name", { length: 255 }).notNull(),
  registrationId: varchar("registration_id", { length: 50 }).unique().notNull(),
  taxId: varchar("tax_id", { length: 50 }),
  groupId: uuid("group_id").references(() => customerGroups.id, { onDelete: "set null" }),
  creditLimit: integer("credit_limit").default(0).notNull(), // in cents for precision
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

// Price list rules assigned to groups
export const priceLists = pgTable("price_lists", {
  id: uuid("id").primaryKey().defaultRandom(),
  name: varchar("name", { length: 255 }).notNull(),
  groupId: uuid("group_id").references(() => customerGroups.id, { onDelete: "cascade" }),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

// Specific items in price list supporting tiered pricing
export const priceListPrices = pgTable("price_list_prices", {
  id: uuid("id").primaryKey().defaultRandom(),
  priceListId: uuid("price_list_id").references(() => priceLists.id, { onDelete: "cascade" }),
  variantSku: varchar("variant_sku", { length: 100 }).notNull(),
  amount: integer("amount").notNull(), // price in cents (USD)
  minQuantity: integer("min_quantity").default(1).notNull(), // for volume discounts
  createdAt: timestamp("created_at").defaultNow().notNull(),
}, (table) => {
  return {
    skuIdx: index("sku_idx").on(table.variantSku),
    priceListIdx: index("price_list_idx").on(table.priceListId),
  };
});

This model allows developers to query prices for a specific client with indexed lookups, removing the need to compute percentage discounts on the application server during page load.


Risk-Free Integrations: How We Handle ERP Outages

A common weak point in e-commerce configurations is a direct, synchronous connection to local ERP/accounting software. Traditional e-shops query ERP APIs synchronously during checkout to verify stock or post orders.

If the internal ERP slows down or returns a 500 Internal Server Error (common during nightly backups or heavy corporate usage), the checkout freezes, the order fails, and payment transaction logs are lost.

We eliminate this problem by building an asynchronous Node.js middleware with a fault-tolerant queue system.

ERP Fault Tolerance:
[ Next.js Checkout ] ──> [ Node.js Middleware / Zod Validation ] ──> [ Redis Queue / BullMQ ]
                                                                             │
             ┌───────────────────────────────────────────────────────────────┘
             ▼ (Successful Attempt)
   [ Local ERP API (QuickBooks/Xero) ]
             │
             ▼ (ERP 500 Failure)
   [ Dead Letter Queue (DLQ) ] ──► [ Automatic Retry with Exponential Backoff ]

How It Works in Practice

  1. Input Validation (Type Safety): All data received from the ERP is validated using the Zod library. This ensures that corrupted data formats (e.g., text values in quantity fields) never compromise the database.
  2. Asynchronous Queues: The order commits to our internal PostgreSQL database right away, confirming success to the customer. It is then placed in a Redis-backed queue (via BullMQ) for background processing.
  3. Dead Letter Queue (DLQ) and Retry: If the ERP API returns an error, the middleware does not mark it as a final failure. The order remains in the queue, and the system executes retries using an exponential backoff strategy. If all retry attempts are exhausted, the task moves to a Dead Letter Queue, and our monitoring stack notifies our support team promptly.

Robust Node.js Middleware Example with Exponential Retry

// src/services/erp-sync.service.ts
import { z } from "zod";

// Zod schema for strict validation of products imported from local ERP
export const ErpProductSchema = z.object({
  id: z.string().uuid(),
  code: z.string().min(1, "Product code cannot be empty"),
  price_usd: z.number().positive("Price must be a positive number"),
  stock_count: z.number().int().nonnegative(),
  vat_rate: z.union([z.literal(20), z.literal(10), z.literal(0)]),
});

export type ErpProduct = z.infer<typeof ErpProductSchema>;

/**
 * Send data to local ERP with exponential backoff retry.
 * Prevents order loss during network drops.
 */
export async function sendOrderToErpWithRetry(
  orderPayload: Record<string, any>,
  retries = 5,
  delayMs = 1000
): Promise<{ success: boolean; erpRef?: string }> {
  try {
    const response = await fetch(`${process.env.ERP_API_URL}/orders`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${process.env.ERP_API_TOKEN}`,
      },
      body: JSON.stringify(orderPayload),
    });

    if (!response.ok) {
      throw new Error(`ERP responded with status code: ${response.status}`);
    }

    const data = await response.json();
    return { success: true, erpRef: data.document_number };

  } catch (error) {
    if (retries <= 0) {
      console.error("Critical failure: Order was not delivered to ERP after 5 attempts. Routing to DLQ.", error);
      // Integration of Slack Webhook or PagerDuty alerting for SLA support
      return { success: false };
    }

    console.warn(`ERP sync failed. Retries remaining: ${retries}. Retrying in ${delayMs}ms. Error: ${(error as Error).message}`);
    
    // Wait with exponential delay increase
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return sendOrderToErpWithRetry(orderPayload, retries - 1, delayMs * 2);
  }
}

This architecture ensures the store remains fully operational for customers even while your office network undergoes maintenance or database synchronization.


Row Level Security (RLS) for Price Isolation at the Database Level

In B2B e-commerce, the exposure of contract pricing to other clients is a major business risk. Traditional monolithic stores filter pricing at the application code level. If a developer makes an error in a WHERE clause or SQL lookup, a customer might see the wholesale rates of another client.

We resolve this directly at the PostgreSQL database level using Row Level Security (RLS). This ensures that the database physically rejects queries for pricing rows that do not belong to the active user's customer group, even if the application-level code contains a bug.

SQL Definition of Row Level Security for B2B Pricing

-- 1. Enable RLS on price list prices table
ALTER TABLE price_list_prices ENABLE ROW LEVEL SECURITY;

-- 2. Create security policy for reading data
-- This policy allows a logged-in customer to see prices only if they belong
-- to the customer group for which the price list is intended.
CREATE POLICY b2b_customer_price_isolation ON price_list_prices
    FOR SELECT
    USING (
        price_list_id IN (
            SELECT pl.id 
            FROM price_lists pl
            JOIN customer_groups cg ON pl.group_id = cg.id
            JOIN customers c ON c.group_id = cg.id
            WHERE c.id = current_setting('app.current_customer_id', true)::uuid
        )
    );

For every database query, our middleware sets the session parameter app.current_customer_id to the ID of the authenticated client. PostgreSQL then filters the data, enforcing database-level isolation.


Comparison Table: SaaS vs. Headless Custom Development

The table below summarizes the key differences between rented platforms and custom headless architectures:

Parameter Traditional Rented SaaS Custom Headless (nolimeo Stack)
Loading Speed (TTFB) Slow response with large databases Much faster via Edge rendering
IP Ownership (Code) None. Renting a third-party license Full ownership of source code and database
B2B Customization Limited to templates and pre-installed plugins Unlimited. Code written specifically to match business processes
Transaction Fees 0.5% - 2% of revenue paid to the platform 0% transaction fees (own infrastructure)
ERP Integration Synchronous (risk of e-shop crash on ERP error) Asynchronous via Node.js middleware and Redis queues
Price Security Filtered at the application level (risk of leaks) Isolated directly at the PostgreSQL database level (RLS)

Conclusion: When is it Time for a Strategic Transition?

Migrating from a rented e-shop to custom headless architecture is a strategic decision requiring initial investment and planning. However, it is the only viable path for companies that have outgrown SaaS limits and where every second of slowdown or system crash translates directly to lost revenue.

If your company is struggling with technical debt, if your current vendor claims complex B2B pricing "is not technically possible," or if you are paying high transaction fees for software you do not own, it is time to transition. Our engineering studio assumes complete responsibility for data migration, infrastructure setup, and custom system deployment.

Interested in an audit of your current system and a custom roadmap to a headless architecture? Contact us and we’ll review your current system, constraints, and migration path.

Interested in pushing your project forward?