18 May 2026 • 14 min read
From Static Catalog to Dynamic Marketplace: How a Specialty Tea Merchant Rebuilt Their E-Commerce Platform
When a third-generation tea merchant sourcing organic single-origin teas from the Nilgiri Hills found that their 8-year-old Shopify store no longer matched the sophistication of their specialty blends or the demands of two distinct buyer audiences, they faced a hard choice: keep patching a broken theme or rebuild from the ground up without pausing live sales. The team chose the harder path — a full-stack overhaul using Next.js 14 with App Router, a shared Shopify REST + Storefront API layer, and two wholly separate front-end portals: one retail storefront for individual tea drinkers and one private bulk portal for café chains and hoteliers purchasing carton and pallet-scale quantities. The solution also introduced a type-safe Turborepo monorepo, a Redis-backed product cache, ISR-driven retail pages, and a blue/green deployment pipeline for zero-downtime launch — all delivered in 8 weeks. The results were decisive: Lighthouse Performance vaulted from 38 to 91, mobile conversion doubled, session abandonment nearly halved, and bulk-channel order share quadrupled. This case study unpacks every decision, constraint, and trade-off from kickoff to launch, and draws out lessons that any mid-market e-commerce team will find directly applicable.
For three generations, Nilgiri Leaf & Co. — a well-regarded specialty tea merchant sourcing organic single-origin teas from the Nilgiri Hills — had served B2C retail buyers through a Shopify store built in 2018. Built by a freelance developer on a tight budget, the foundational store had performed adequately through 2022. But by late 2024 the business had outgrown it: the Shopify theme could not accommodate a bulk-order cart, the product schema could not distinguish between leaf grades, and the site scored a 38 on Lighthouse performance — a hard ceiling for SEO, mobile traffic, and conversion.
In January 2025, the merchant brought on a full-stack engineering team with a single mandate: redesign, rearchitect, and re-launch the e-commerce platform without interrupting live sales. The result was a dual-portal Next.js application connecting to a headless Shopify backend — one storefront for individual tea drinkers and one private portal for café and hotel bulk buyers — deployed with zero downtime and a 43-point Lighthouse jump in 8 weeks.
Overview
Nilgiri Leaf & Co. operates across two distinct channels. Their retail channel sells curated boxes of single-origin black, green, white, and flavoured teas to individual consumers through SEO-optimised product pages and an email-fuelled subscription program. Their bulk channel supplies café chains, boutique hoteliers, and regional restaurants with kilogram-scale cartons, pallet pricing sheets, seasonal availability calendars, and purchase-order workflows. Before the rebuild, both channels lived on the same legacy Shopify theme. The bulk customers had been tolerating a clunky workaround for years — requesting quotes by email and phoning in purchase orders — because the platform simply could not model a cart-by-carton purchase flow or tiered pallet pricing.
The rebuild was scoped around three deliverables that the legacy system could not deliver: a modern consumer storefront, a private B2B portal with custom bulk workflows, and a shared headless Shopify backend that treated both channels as a single inventory truth.
The Challenge
The problems were not aesthetic. They were structural, compounding over years of incremental patches.
Performance Debt
Lighthouse audits consistently returned a 38 performance score. The theme carried a combined 620 KB of unminified JavaScript and CSS, most loaded synchronously on every page. The {% if wasShopify %}theme-dependent{% endif %} asset pipeline prevented hotel buyers on slower rural connections — a material segment of the bulk channel — from completing a product browse within 5 seconds. Shopify's own reporting flagged a 23 % session-abandonment rate on product listing pages.
Architecture Limitations
The Shopify theme engine rigidly couples content to layout. There was no way to model a bulk product variant that treated cartons (250 g) and pallets (10 kg) as distinct purchase units with different per-unit pricing, without abusing Shopify's variant limits or duplicating products across collections. Bulk buyers who needed PO numbers, delivery windows, GST invoice fields, and seasonal availability visibility received no first-class support from the platform's built-in checkout. Every constraint hit revenue.
Mobile Experience
68 % of retail sessions came from mobile. The theme was a desktop-first layout stretched to 375 px with media-query patches that introduced horizontal scrolling and broken hover-states. Shopify's mobile analytics reported a conversion rate of 0.54 % on mobile — roughly half of the 1.02 % desktop conversion rate. The gap was symptomatic, not incidental: shoppers on mobile were physically unable to evaluate teas as well as shoppers on desktop, because the product grid did not reflow into a touch-friendly interface.
Content Fragmentation
The merchant manually maintained three digital surfaces — the Shopify store, a WordPress blog, and a WhatsApp broadcast list. Product copy, brewing instructions, and tasting notes were copy-pasted into each without a canonical source. A seasonal blend could be described correctly on the Shopify page but incorrectly on WordPress before anyone caught it. Content drift was inevitable; it was merely a matter of when.
Goals
The goals were agreed at kickoff, before a single line of code was written, and re-measured at every two-week sprint review.
- Performance: Achieve a Lighthouse Performance score of ≥ 85 across all pages, measured on simulated 4G throttled hardware.
- Retail Conversion: Lift mobile conversion rate from 0.54 % to ≥ 0.80 % within 12 weeks of launch.
- Bulk Channel Lift: Reduce the bulk inquiry friction by replacing email/phone quoting with a first-class self-service cart and checkout for pallet-scale orders.
- Uptime: Zero downtime or data loss during migration for a live store processing an average of 42 orders daily.
- Developer Maintainability: Establish a typed codebase with a CI/review workflow so the merchant's in-house team could take over features without re-engaging the consultancy.
Approach
Tech Stack Decision
The team evaluated three directions: a new Shopify-os-2.0 theme, a WordPress + WooCommerce build, and a headless Next.js + Shopify-storefront-API build. The winning direction was chosen in two rounds. First, technical examiners agreed that anything built on Shopify's Liquid templating layer would inherit the same asset-bundling and performance constraints that had produced the current theme's 38 Lighthouse score. Second, headless Shopify was rejected for the retail storefront — the Storefront GraphQL API's per-request billing model would add unpredictable cost as traffic grew, and the merchant had no appetite for managing an extra billing layer. The compromise: Next.js 14 / App Router retail storefront backed by Shopify REST API for authentication-routed operations, and a headless Shopify Storefront API layer for real-time product sync and inventory availability — the only headless call paths were read-only and cached aggressively through CDN.
TypeScript was non-negotiable from day one. The merchant's in-house developer would inherit the codebase; untyped JavaScript was considered technically irresponsible given the team changeover risk.
Monorepo Architecture
Two Next.js applications — apps/retail and apps/bulk — were created in a Turborepo monorepo alongside a shared packages/shopify package for all API interactions. This design has two practical consequences. First, both portals share one set of Shopify integration types and utilities, eliminating the divergence risk of having two independent client configurations. Second, both share the same components/ui primitives (buttons, modals, error boundaries), reducing per-line maintenance.
Retail Portal Architecture
The retail portal is a Next.js 14 App Router application optimised for retail SEO: product pages are statically generated at build time using Shopify's /products/{handle} REST endpoint and revalidated on a one-hour ISR window. Structured data and Open Graph tags are emitted server-side for every page type. The core retail components are:
- ProductGrid — a client-side masonry grid with instant type and region filtering, built with CSS Grid so that additional filter categories can be added without touching layout markup.
- ProductDetail — a static server component that fetches product data at build time, supported by a React Suspense boundary so that price metadata loads independently of the image gallery.
- CartDrawer — a slide-out drawer built with Headless UI's transition primitives, keeping the cart accessible on mobile without navigating away from the search flow.
- CheckoutRouter — routes authenticated and guest checkout paths by parity-checking the buyer's session cookie, proxying only the session-token exchange to Shopify's REST checkout endpoint.
B2B Portal Architecture
The bulk portal runs on a subdomain and is ineligible for Shopify's native customer segmentation, so all pricing and availability logic is resolved at the application layer from in-house product data tables. The core bulk components are:
- CartByCarton — adds and removes whole cartons (250 g baseline) or pallets (10 kg / 40 cartons) as atomic line items, preventing fractional quantities and price per gram drift.
- TierPricingPanel — reads from rate cards keyed to buyer tier (Gold / Silver / Bronze), quantity thresholds, and SKU, displaying per-carton and per-kilo rates side-by-side. Rate cards are managed in a headless CMS and overridden on a per-buyer basis.
- POUpload — accepts image-based POs using a lightweight OCR hint extractor, populating fields (PO number, expected delivery date, reference contact) into the checkout form before submission.
- AvailabilityCalendar — fetches forward-looking availability windows per SKU from the merchant's harvest planning sheet (scheduled as a build-time data fetch) and displays either a plain date range or a notify-me form depending on current stock level.
Implementation
Phase 1 — Data Model & API Contract
Weeks 1–2 focused on the shared types and Shopify integration package before any UI was built. The reason is practical: rendering a page against an API that does not yet return the fields it requires produces compounding rework. The team defined 12 Shopify metafield namespaces covering retail attributes (tasting notes, regional origin, brewing temperature, certifications) and bulk attributes (carton SKU, pallet SKU, available window, reorder holdover). Each metafield type, required fields, and rejection rules were documented in a shared schema file so that the merchant's product team could populate Shopify before handoff. The Shopify REST endpoints used are /admin/api/2025-01/products.json, /admin/api/2025-01/inventory_levels.json, and /admin/api/2025-01/checkouts.json, each wrapped in typed functions in packages/shopify/src/ with retry logic for 429 responses.
Phase 2 — Retail Portal Core
Weeks 3–4 delivered the fully functioning retail storefront using Next.js ISR for product listing and detail pages. The key architectural challenge was performance within Shopify's REST rate limits. The solution was read-through Redis caching of product batches. The Shopify REST API is hit only on cache misses; ISR revalidation handles the subsequent write. This brings the effective API call rate on the retail storefront from an estimated one call per session to one call per product SKU per hour. Cart operations are all client-side until checkout, at which point a single server-side token exchange with Shopify eliminates any session-managed auth complexity from the client.
Lighthouse performance was measured at the end of Phase 2 on a CI runner. The combination of build-time data generation, CDN edge caching, and Lighthouse-esque image loading pushed the score from the baseline 38 to 87. Dissatisfyingly close to the conservative target; the team immediately identified critical.css extraction for the cart and header as the next unlock.
Phase 3 — Bulk Portal Core
Weeks 5–6 built the B2B portal as a private Next.js application behind a CloudFlare Access allow-list. Only verified email domains matching the merchant's café and hotel business list are permitted past the access gate. The buyer account system (email address → buyer tier → rate cards) is maintained entirely within the application, not using Shopify Customer Accounts, because Shopify Customer Tags cannot be used in checkouts without the Shopify Plus plan and Shopify Plus B2B features have monthly licensing costs that the merchant declined.
The 250 g / 10 kg dual-unit system was implemented as two separate Shopify variants per SKU with inventory synced independently. The REST API endpoint /inventory_levels is polled via a once-per-hour Node-Cron job running inside a Heroku worker dyno, updating the application's in-house rate card store. This decouples every bulk checkout from Shopify's checkout duration limits, because the critical path — rate card lookup, checkout creation, PO upload, payment confirmation — runs entirely within the application before calling Shopify only for final fulfilments.
Phase 4 — Migration & Go-Live Plan
Week 7 was dedicated to production hardening, staging sign-off, and a DNS cutover plan. The migration strategy used a blue/green deployment via CloudFlare Wrangler. Two instance pools ran simultaneously — one serving the frozen Shopify theme, the other serving the Next.js app. A CloudFlare Worker script routed 1 % of traffic to the new instance on launch day, increasing in 25 % increments every four hours, with automated rollback triggered by either a 15-minute error-rate spike above 3 % on the new pool or an SLA breach on checkout-success latency. Checkout state was not exchanged between pools; any order started on the old theme completed entirely on the old theme, and any SEO equity remained with the original URLs.
Phase 5 — Developer Handoff
Week 8 was a three-day structured handoff session covering: the Turborepo project structure, the Shopify integration directory, the CloudFlare Worker routing model, and the Heroku worker-cron update cycle. The merchant's developer was given access to the repository two weeks before go-live, so the handoff built on hands-on familiarity rather than on a single Q&A session. At the end of the week the repository moved to the merchant's own GitHub Organisation and the engineering team's access was rotated out — a critical security hygiene step that was checked into the runbook before anyone left.
Results
Four weeks after launch, every published success criterion had been met or exceeded.
| Metric | Pre-Rebuild | Post-Rebuild | Change |
|---|---|---|---|
| Lighthouse Performance Score | 38 | 91 | +53 pts (+139 %) |
| Mobile Conversion Rate | 0.54 % | 1.12 % | +108 % |
| Desktop Conversion Rate | 1.02 % | 1.68 % | +65 % |
| Bulk Order Rate (% of total orders) | 2.1 % | 8.4 % | +300 % |
| Session Abandonment (retail) | 23 % | 12 % | −11 pts (−48 %) |
| Search-Index Keywords in Top 20 | 17 | 54 | +217 % |
| Time to First Byte (p95) | 1,840 ms | 162 ms | −91 % |
The SEO lift is as direct as the performance lift: Google's Core Web Vitals are a confirmed ranking factor, and the merchant has fragmented but documented evidence from multiple sources suggesting the LCP and INP improvements have lifted organic placement for 37 of the 54 terms that rank in the top 20. The content editing workflow is now decoupled from the front end: the merchant uses Shopify's metafield editor for product content and a markdown editor in their headless CMS for blog content, with none of the copy-paste drift of the pre-build era.
Bulk channel adoption surprised no one at the merchant but surprised the consultancy team. Bulk checkouts are now at 8.4 % of total order count, a fourfold increase against 2.1 % baseline — the analyst attribute this not merely to the new self-service flow but to a change in buyer behaviour: since pallet pricing is now visible before checkout rather than hidden behind an email quote, mid-tier cafés that would formerly have delayed or cancelled large orders are now completing them directly in a single session. Customer support ticket volume from bulk buyers has dropped by an estimated 76 %, precisely because no-one has to call for a quote anymore.
Key Lessons
Lesson 1: Investment in foundational types pays compound interest
Building packages/shopify as a typed API layer first — before a single page was rendered — meant that the bulk portal was assembled from a list of checked-off types rather than reverse-engineering API shape exceptions after every integration point failed. The cost of a type-first start was two weeks of engineering before anything visual shipped; the return was a second portal that had to add only two new package functions. Hunting for untyped integration shapes across a codebase that already shipped is exponentially more expensive.
Lesson 2: ISR beats static generation when your data changes more than your code
The team over-engineered the retail product pages as a hybrid of static generation and ISR (Incremental Static Regeneration) to balance build-time speed against data freshness. Production data validated the choice: if the merchant launches three seasonal teas and two refurbishes on any one day, the once-per-hour ISR revalidation keeps all retailer pages up to date without requiring a full rebuild. The baseline ISR hit rate has settled at roughly 40 % of all product page requests hitting prepopulated cache, enough to shelter Shopify's rate limits during the holiday peak without compromising freshness.
Lesson 3: A migration plan is not a deployment script — it is a failure model you smoke-tested
The blue/green deployment plan was stress-tested in staging for 48 hours before launch. The team seeded the staging environment with 10,000 synthetic sessions running checkout transactions — both single-carton retail and 40-carton bulk — with injected latency to simulate rural connections. The rollback thresholds of 15-minute error-rate above 3 % and checkout-p95-latency above 1,200 ms were calibrated against synthetic failure injections rather than guessed from averages. In practice, the rollout required no rollback; the last-resort know-how of having one was itself the insurance reason the engineering chief slept during launch night.
Lesson 4: Developer handoff is a deliverable, not an afterthought
Merchants buy a product, not a building. But when the product is software, the handover is the difference between sustainably positive and costly maintenance. The three-day handoff session, the two-week pre-go-live repository access period, and the explicit permission-rotation step were not management theatre — they prevented at least two major incidents from compound errors during the merchant's own deployment attempts in the following two weeks.
Conclusion
The rebuild was not simply a design refresh. It was a systematic dismantling of a performance debt, an architecture debt, and a developer workflow debt — all three paid down simultaneously across a single eight-week sprint. The results speak: a 139 % improvement in Lighthouse performance, a doubling of mobile conversion, and a four hundred percent growth in bulk-channel revenue were all achieved without a day of sales downtime.
The lessons that stick are the structural ones: types first, typed at the boundary, ISR measured against data-change frequency rather than build time, migrations stress-tested against a named failure model, and a handoff plan that treats developer capability transfer as a concrete deliverable rather than a receipt.
For the merchant, the platform is no longer a constraint on growth. It is the layer that directly services the ambitious sales, SEO, and international expansion they had been steadily signalling from 2024 onward. For the engineering team, the architecture is a demonstration of how a headless e-commerce build can sit within a Shopify subscription without inheriting its platform layer's performance and architecture debt — a design pattern worth revisiting for any mid-market merchant facing the same rebuild question.
