A Sivakasi-based crackers aggregator — 9 family-owned manufacturers feeding one storefront — came to us in early August 2025 with one specific window. They needed to be live by October 14, ship through Diwali (October 20, 2025), and not over-promise on a single SKU. We built the marketplace in 9 weeks. They opened on October 11. By Diwali night they had processed ₹8.4 cr GMV across 11,420 orders, with split-shipments to 1,840 PIN codes and zero stockouts. Here is how the split-shipment logic worked, the per-vendor SLA enforcement, the WhatsApp tracking flow, and the 4 design decisions that earned the festive window.
9
Sellers (Sivakasi family manufacturers)
₹8.4 cr
GMV across the 11-day festive window
1,840
PIN codes shipped (TN, KA, AP, KL, MH)
0
Stockouts during the festive window
## The Answer in 60 Words
We built a Next.js + PostgreSQL marketplace where every cart can contain SKUs from up to 9 sellers, but the customer sees one checkout, one payment, one order ID. Behind the scenes the order is atomically split into per-vendor sub-orders with per-vendor SLAs. WhatsApp-based tracking pushes one consolidated update per shipment milestone. Per-vendor inventory is mirrored from each manufacturer's existing Excel/Tally setup via a simple Google Sheets bridge.
## Why This Matters Now
Indian crackers e-commerce is hyper-seasonal. From October 1 to October 22, 2025 the category saw a 47x lift over a normal month. Single-vendor stores hit ceiling capacity within days. The aggregator model — 9 sellers pooling under one storefront — works only if the technical plumbing handles split shipments, partial failures, and per-vendor SLAs cleanly. Our client had been on a Shopify multi-vendor app for Diwali 2024 and had spent the post-Diwali week refunding ₹14 lakh in over-promised orders. They asked for a custom build with one ask:
tell us in real time when a single seller cannot fulfil — never let the customer hear it first.
## The Client (Specific Details)
-
Sector: Diwali crackers aggregator (sparklers, anaars, ground chakras, aerial shells)
-
Location: Sivakasi, Tamil Nadu (the crackers manufacturing hub)
-
Sellers: 9 family-owned manufacturers, all within 35 km of each other
-
Customers: ~22,000 households, mostly Karnataka, Andhra, Tamil Nadu, with ~14% in Kerala and Maharashtra
-
Stack on day 0: A Shopify Plus store with the Shipturtle multi-vendor app, the 9 sellers each running their own Excel inventory
-
Trigger: The 2024 Diwali post-mortem identified ₹14 lakh in refunds from oversold SKUs. The CEO drew a line.
## The 4 Design Decisions That Earned the Festive Window
1
One Cart, N Sub-Orders
The customer sees one cart, one total, one payment. The system creates one parent Order and N child SubOrder rows (one per seller). Payment is captured at the parent level; payouts to sellers are reconciled per child order.
2
Per-Vendor SLA Enforcement
Each seller signs an SLA: dispatch within 18 hours of payment. Our scheduler watches every sub-order. At 12 hours unfulfilled, an automatic WhatsApp nudge to the seller. At 18 hours, the order auto-cancels with a customer credit note.
3
Atomic Inventory Reservation
When the customer hits Pay, we reserve stock atomically across all 9 sellers via a single Postgres transaction with row-level locks. If any seller is short, the cart fails fast — before the payment is initiated. Same pattern as our
sweet-shop chain inventory sync.
4
Consolidated WhatsApp Tracking
If 3 sellers ship 3 sub-orders, the customer gets 3 dispatched messages on the same WhatsApp thread, one per shipment, but only one order-confirmation message. The thread is grouped by parent Order ID via WhatsApp Business API template.
## The Architecture
FE
Next.js 14 App Router + Tailwind + Razorpay UPI Intent
API
tRPC + NextAuth + per-seller RBAC
DB
PostgreSQL 16 (Order ⇒ N×SubOrder ⇒ N×LineItem)
SY
Google Sheets bridge per seller (Apps Script webhook)
WA
WhatsApp Business API via Gupshup (per-thread grouping)
## The Split-Shipment Schema (The Core Pattern)
-- Parent order: one per customer purchase
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID REFERENCES customers(id),
total_amount NUMERIC(10,2) NOT NULL,
payment_id TEXT,
payment_status TEXT NOT NULL DEFAULT 'pending',
whatsapp_thread_id TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Sub-order: one per seller within an order
CREATE TABLE sub_orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
parent_order_id UUID NOT NULL REFERENCES orders(id),
seller_id UUID NOT NULL REFERENCES sellers(id),
sub_total NUMERIC(10,2) NOT NULL,
status TEXT NOT NULL DEFAULT 'reserved', -- reserved, dispatched, delivered, cancelled
sla_dispatch_by TIMESTAMPTZ NOT NULL,
dispatched_at TIMESTAMPTZ,
awb TEXT,
carrier TEXT,
cancelled_at TIMESTAMPTZ,
cancellation_reason TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (parent_order_id, seller_id)
);
-- Line item: one per SKU within a sub-order
CREATE TABLE line_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sub_order_id UUID NOT NULL REFERENCES sub_orders(id),
sku_id UUID NOT NULL REFERENCES skus(id),
qty INT NOT NULL CHECK (qty > 0),
unit_price NUMERIC(10,2) NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Index on the sub-order SLA for the watcher
CREATE INDEX idx_sub_orders_sla_pending
ON sub_orders (sla_dispatch_by)
WHERE status = 'reserved' AND dispatched_at IS NULL;
The atomic reservation is one transaction:
BEGIN;
-- Row-lock all the SKU rows we are about to decrement
SELECT id FROM sku_inventory
WHERE id IN ($1, $2, $3, ..., $n)
FOR UPDATE;
-- Check stock for every line. If any short, ROLLBACK.
SELECT id, available_qty FROM sku_inventory WHERE id IN (...);
-- (application logic verifies all >= requested)
-- Decrement stock per SKU
UPDATE sku_inventory SET available_qty = available_qty - $qty WHERE id = $sku_id;
-- Create the parent order, sub-orders, line items
INSERT INTO orders (...) VALUES (...);
INSERT INTO sub_orders (...) VALUES (...); -- one per seller
INSERT INTO line_items (...) VALUES (...);
COMMIT;
We considered Redis-based reservations (the pattern from our sweet-shop case study) but for a marketplace with multi-row, multi-seller atomicity needs, Postgres SELECT FOR UPDATE was cleaner. Read about
PostgreSQL explicit row locks for the semantics.
## The Per-Vendor SLA Watcher
The single most-asked-for feature: a 4-line Postgres query that drives a Node scheduler running every 60 seconds.
// Every 60 seconds: nudge sellers approaching SLA, cancel sellers past SLA
async function slaWatcher() {
const now = new Date();
// Sellers approaching SLA: send a WhatsApp nudge at 12h
const nudgeWindow = await db.query(
SELECT so.id, so.parent_order_id, so.seller_id, so.sla_dispatch_by
FROM sub_orders so
WHERE so.status = 'reserved'
AND so.sla_dispatch_by BETWEEN $1 AND $2
AND NOT EXISTS (SELECT 1 FROM sla_nudges WHERE sub_order_id = so.id)
, [now, new Date(now.getTime() + 6 3600 1000)]);
for (const so of nudgeWindow.rows) {
await sendWhatsAppToSeller(so.seller_id, Order ${so.parent_order_id} dispatch SLA in 6 hours);
await db.query(INSERT INTO sla_nudges (sub_order_id, sent_at) VALUES ($1, now()), [so.id]);
}
// Sellers past SLA: auto-cancel and credit-note the customer
const breached = await db.query(
UPDATE sub_orders
SET status = 'cancelled',
cancelled_at = now(),
cancellation_reason = 'sla_breach_auto'
WHERE status = 'reserved'
AND sla_dispatch_by < $1
RETURNING id, parent_order_id, seller_id
, [now]);
for (const so of breached.rows) {
await rollbackInventoryForSubOrder(so.id);
await issueCreditNote(so.parent_order_id, so.id);
await sendWhatsAppToCustomer(so.parent_order_id,
Sorry — one of our sellers could not dispatch your order. Full refund processed in 4-6 hours.);
}
}
The auto-cancellation was the most controversial design decision with the sellers. We softened it by adding the 12-hour nudge window. In the festive run, the watcher fired 38 nudges and only 4 auto-cancellations across 11,420 orders. Sellers learned the SLA was real after the first 2 auto-cancellations on day 3.
## The 9-Week Build Plan
1
Weeks 1–2: Discovery + seller onboarding doc
Met all 9 sellers in person at Sivakasi. Catalogued 184 SKUs across all sellers. Wrote a one-page SLA in Tamil + English that every seller signed. Each seller gave us their existing Excel inventory in their own format.
2
Weeks 3–4: Schema + Google Sheets bridge per seller
Postgres schema with the order/sub-order/line-item split. Per-seller Google Sheets template with an Apps Script webhook that syncs inventory to our Postgres every 5 minutes. Sellers update their Sheet from their phones — no new tool to learn.
3
Weeks 5–6: Storefront + cart + atomic checkout
Next.js storefront with category browse + per-seller filters. Cart that visualises sub-orders by seller. Atomic checkout flow with Razorpay UPI Intent (the recommended
UPI Intent integration for higher success rates).
4
Week 7: WhatsApp tracking + SLA watcher
Gupshup integration for the 4 message templates: order confirmed, dispatched, out for delivery, delivered. SLA watcher cron with the 12h nudge + 18h auto-cancel logic. Tested with 200 mock orders.
5
Week 8: Load testing + seller training
k6 load test at 6x projected peak: 4 race conditions found in the sub-order creation path, all fixed. Day-long Tamil-language training session in Sivakasi for all 9 sellers + 14 of their staff.
6
Week 9: Soft launch + Diwali ramp
Opened to existing customers on Oct 11. 412 orders the first day. Caught one carrier-AWB-format bug (Trackon vs DTDC formats). Patched. Full marketing push from Oct 14.
## What Happened Through Diwali Week
Outcome (Oct 11–22, 2025): 11,420 parent orders. 18,640 sub-orders (avg 1.63 sellers per cart). 1,840 unique PIN codes shipped. 4 SLA-driven auto-cancellations. Zero customer-discovered stockouts. ₹8.4 cr GMV. Refund liability: ₹38,400 (4 cancellations × ₹9,600 avg).
## Pre-Festive Readiness Checklist (Marketplace Edition)
- Per-seller Google Sheets bridge tested with each seller's actual data structure
- Atomic reservation tested with 50 concurrent buy threads against a single SKU
- SLA watcher tested with mock breached orders (12h nudge fires, 18h auto-cancel fires)
- WhatsApp templates approved by Meta in the BSP console (Gupshup, AiSensy, etc.)
- Razorpay route tested with UPI Intent fallback to UPI Collect
- Per-seller payout reconciliation report tested for the post-Diwali settlement run
- Carrier AWB-format compatibility tested per shipping partner (Trackon, DTDC, BlueDart, Delhivery)
- Customer credit-note flow tested with Razorpay refund API
- On-call rota set for 18 days (Oct 5–22) with 2 engineers per 8-hour shift
- Roll-back plan documented for every config change in the last 14 days
## Common Mistakes (Each One Hurts)
Symptom: "Customer pays but the order is not created." Cause: Razorpay webhook arrives before the inventory transaction commits. Fix: webhook handler retries with idempotency key for 30 minutes. Always.
Symptom: "One seller is constantly missing SLA." Cause: that seller has more SKUs than they can pack within their own constraints. Fix: cap the number of orders the marketplace routes to that seller per hour. Make the cap explicit in the SLA.
Symptom: "Two sellers ship duplicate items because the customer thinks one shipment failed." Cause: WhatsApp tracking shows separate dispatched messages and the customer panics. Fix: the order-confirmed WhatsApp explicitly says "your order has been split across N sellers; you will receive N shipments. This is normal."
Symptom: "Inventory drifts between Google Sheets and Postgres." Cause: a seller updates their Sheet during a checkout transaction. Fix: the Sheets bridge writes to a staging table; the SLA watcher reconciles staging to inventory every 90 seconds.
Symptom: "Settlement to sellers is wrong by ₹4-7 per order." Cause: the marketplace fee is calculated on parent total but settlement is per sub-order — rounding diverges. Fix: compute fees per sub-order; the parent total is the sum.
## When NOT To Build a Custom Marketplace
Skip the custom build if (a) your seller count is under 4 and unlikely to grow — Shopify multi-vendor apps are honestly fine, (b) your festive uplift is under 5x normal trade — the engineering does not pay back, or (c) your sellers cannot maintain their own inventory data. We have walked away from 2 marketplace projects in 2 years for exactly this reason — the 4 sellers were each "we will figure it out" on inventory and the math did not work. Honest call.
## A Detail That Saved Us On Diwali Eve
On October 19 (Choti Diwali) at 7:42 pm, the WhatsApp Business API rate-limited us. We were sending ~340 messages per minute and Gupshup throttled us to 220. The dispatched-shipment messages started queuing. Customers panicked because they could see the AWB on the storefront but no WhatsApp. We caught it in 14 minutes by watching the Gupshup webhook latency dashboard. Fix was to bucket messages into 60-second windows and prioritise order-confirmed > dispatched > delivered. Recovery in 8 minutes. The lesson: festive WhatsApp volume needs separate rate-limit testing — it is the one provider integration that throttles aggressively.
## FAQ
### Why Postgres SELECT FOR UPDATE instead of Redis DECRBY?
For a multi-row atomic reservation across multiple sellers in a single cart, Postgres row-level locks are simpler than coordinating multiple Redis DECRBYs with rollback. Redis is faster for single-key decrements (our
sweet-shop case study uses Redis). Marketplaces with cross-vendor atomicity favour Postgres.
### How did you handle WhatsApp template approval?
We submitted 4 templates to Meta via Gupshup 6 weeks before launch. 3 were approved in 48 hours. The fourth (the auto-cancel apology) was rejected once for tone, revised, and approved on resubmission. Always plan a 2-week buffer for WhatsApp template approval before a festive sale.
### What is the marketplace fee structure you used?
A flat 9% per sub-order, paid by the seller, deducted at settlement. Some sellers asked for a graduated tier (lower at higher volumes); we kept it flat for the first season for simplicity. Phase 2 will add a tier.
### Did you build seller-side dashboards?
A simple Next.js dashboard per seller showing their sub-orders, dispatch status, settlement summary. We did not build a full ERP — sellers continue to run their factory operations on Excel. The dashboard is order-acceptance and dispatch only.
### How did you handle returns and refunds?
For crackers, returns are essentially zero (perishable + dangerous). The refund flow is for cancellations and damaged-in-transit. Razorpay refund API + a manual seller-debit for cancellations driven by seller fault.
### What about ONDC integration?
We considered it. The aggregator wanted to focus on direct sales for the first season and revisit ONDC in 2026. The architecture supports it (the order/sub-order schema maps cleanly to ONDC's split-fulfilment model).
### What was the team for this build?
Three full-stack engineers (one lead on schema + atomicity, one on storefront + cart, one on WhatsApp + payment integrations), our QA lead Manvi at 0.5 FTE for the load-testing pass, our designer at 0.3 FTE for the seller dashboard. Our CTO
Hrishikesh at 0.3 FTE for architecture review and the festive war-room rota.
## Want a Marketplace for Your Seller Network?
Need a Multi-Vendor Marketplace for Your Festive Seller Network?
We build custom multi-vendor marketplaces for Indian aggregators in the 5-25 seller range. Typical engagement: 8-12 weeks, fixed-price ₹6L-₹16L. We do the festive war-room with you on launch year. First call is with the engineer who would lead your build.
Book a Marketplace Scoping Call
Related reading: our
12-outlet sweet shop inventory sync, our
Amazon GIF day-1 stack audit, the
Chhattisemanditak farmer marketplace, and our
web development service.
Email contact@softechinfra.com to receive the marketplace schema + SLA template before the call.