Shopify DTC Outdoor & Lifestyle · 9 weeks · STAR method

Custom Shopify App That Eliminated Stockouts and Saved 20 Hours of Manual Work Per Week

A brand like YETI · DTC Outdoor & Lifestyle

87%
Stockout reduction
$580k
Annual revenue recovered
3 hrs → 15 min
Daily manual work eliminated
2,400+
SKUs monitored

Situation

The brand had built a loyal following for a core range of outdoor drinkware. Their top 15 SKUs — specific colourways and sizes of their flagship products — accounted for 60% of revenue. When those SKUs stocked out, revenue dropped sharply. Loyal customers who wanted the exact product didn’t substitute; they waited, or bought from an authorised retailer at a margin hit.

The stockout problem had three root causes:

1. Manual monitoring lag. The merchandising coordinator pulled inventory reports from Shopify every morning, compared against a spreadsheet of reorder points, and flagged items manually. By the time a reorder was raised, the item was often already at critically low stock — or out.

2. No lead time intelligence. Reorder points were static numbers set at the start of each season. They didn’t account for supplier lead times, which varied by 2–6 weeks depending on the factory and shipping method.

3. No velocity awareness. A product going viral on social could burn through six weeks of stock in 72 hours. The static reorder system had no way to detect velocity spikes until it was too late.


Task

Build a custom Shopify app that:

  • Monitors all inventory levels continuously against dynamic reorder thresholds
  • Calculates reorder quantities based on velocity, lead time, and target cover days
  • Alerts the merchandising team via Slack and email with pre-filled purchase order drafts
  • Provides a dashboard showing current cover days, projected stockout dates, and pending reorders
  • Integrates with their three primary suppliers’ APIs for real-time lead time data

The app needed to be production-ready, with proper error handling and audit trails, and operable by non-technical staff.


Action

Architecture

We built the app using Shopify’s Remix framework and the Shopify CLI. The backend runs on Railway. The core components:

┌─────────────────────────────────────────────────────────────┐
│                    Shopify Admin App                         │
├──────────────────┬──────────────────┬───────────────────────┤
│  Inventory       │  Reorder Engine  │  Supplier Integration │
│  Monitor         │                  │                        │
│  (Webhooks)      │  - Velocity calc │  - Supplier A API      │
│                  │  - Lead time adj │  - Supplier B API      │
│  inventory_      │  - Cover days    │  - Supplier C EDI      │
│  levels/update   │  - PO generation │                        │
└──────────────────┴──────────────────┴───────────────────────┘
         │                    │                    │
         └──────────────┬─────┘                    │
                        ▼                          │
              ┌─────────────────┐                  │
              │   Alert Engine  │◄─────────────────┘
              │  (Slack + Email)│
              └─────────────────┘

Step 1 — Inventory Monitoring via Webhooks

Instead of polling the Inventory API (which would burn API quota and be slow), we subscribed to inventory_levels/update webhooks. Every inventory change — sale, return, manual adjustment, fulfilment — triggers the reorder engine in near real-time.

// Webhook handler — inventory_levels/update
export async function action({ request }) {
  const payload = await request.json();
  const { inventory_item_id, location_id, available } = payload;

  // Look up the variant and product
  const variant = await db.variants.findByInventoryItemId(inventory_item_id);
  if (!variant) return json({ ok: true }); // not a tracked SKU

  // Run reorder check
  await checkReorderThreshold(variant, available);

  return json({ ok: true });
}

Step 2 — Dynamic Reorder Threshold Calculation

The most important piece. Static reorder points fail because they don’t adapt. We built a nightly recalculation job using a rolling 28-day velocity:

async function calculateReorderPoint(variantId) {
  // 28-day rolling sales velocity (units/day)
  const velocity = await getSalesVelocity(variantId, 28);

  // Supplier lead time for this SKU (from supplier API, or fallback to historical avg)
  const leadTimeDays = await getLeadTime(variantId);

  // Safety stock = 7 days of velocity (buffer for demand spikes)
  const safetyStock = Math.ceil(velocity * 7);

  // Reorder point = stock needed to cover lead time + safety stock
  const reorderPoint = Math.ceil(velocity * leadTimeDays) + safetyStock;

  // Reorder quantity = 60 days of cover (adjustable per SKU)
  const reorderQty = Math.ceil(velocity * 60);

  return { reorderPoint, reorderQty, velocity, leadTimeDays, safetyStock };
}

This means a SKU that’s selling 10 units/day with a 21-day lead time gets a reorder point of (10 × 21) + 70 = 280 units. One that’s selling 2 units/day with a 14-day lead time gets (2 × 14) + 14 = 42 units.

Step 3 — Velocity Spike Detection

For social virality protection, we added a short-window velocity check that runs hourly:

// If 48-hour velocity is > 2.5x the 28-day average → spike alert
const recentVelocity = await getSalesVelocity(variantId, 2); // 48h
const normalVelocity = await getSalesVelocity(variantId, 28);

if (recentVelocity > normalVelocity * 2.5) {
  await sendSpikeAlert(variant, {
    currentStock: available,
    projectedStockoutHours: Math.floor(available / (recentVelocity / 24))
  });
}

This caught a viral TikTok moment three weeks after go-live. A creator posted a video featuring the brand’s most popular colourway, driving 400% normal velocity for 36 hours. The spike alert fired 4 hours in — giving the merchandising team time to contact the supplier and expedite an existing order. Previous system: they’d have found out when the product sold out.

Step 4 — Slack and Email Alerts with Pre-Filled POs

Every reorder alert includes a Slack message with:

  • Current stock level and days of cover remaining
  • Recommended reorder quantity with reasoning
  • Supplier name and current quoted lead time
  • One-click link to confirm the PO (or edit quantities)

The PO draft is generated automatically in the app and can be sent to the supplier with a single button press, or exported to PDF for approval workflows.

Step 5 — Admin Dashboard

A Polaris-based dashboard showing:

ColumnData Shown
SKU / ProductName, variant, current stock
Cover DaysDays of inventory remaining at current velocity
Stockout DateProjected date, colour-coded (red < 14d, amber < 30d)
VelocityUnits/day (7d and 28d rolling)
Reorder PointCurrent dynamic threshold
StatusOn track / Alert / Critical / PO Pending

Result

Three months post-launch:

Stockouts across the hero SKU range dropped from 3–4 per month to an average of 0.4. The one remaining stockout in month two was a force majeure situation (supplier factory closure) that no automated system could have prevented.

Revenue impact: The merchandising team estimated that each stockout event cost $15–20k in lost sales (based on velocity at time of stockout × average days to restock). At 3.5 stockouts/month previously, that was roughly $630k annually. Post-launch: roughly $50k remaining.

Operational impact: The daily manual inventory check went from a 2–3 hour morning task to a 10–15 minute review of the app dashboard and Slack alerts. The coordinator described it as “going from being a spreadsheet janitor to actually doing merchandising strategy.”

The spike detection feature has fired three times in four months — each time giving the team 4–12 hours of advance warning before stock would have run out.

Off-the-shelf inventory apps can handle simple threshold alerts. But dynamic reorder points that account for velocity, lead time, and demand spikes require custom logic — and that’s exactly where the ROI is. If your hero SKUs stockout even once a quarter, a custom app pays for itself.

Ready to Fix Your Analytics?

Get a free 30-minute consultation with our team. No pitch, just honest advice on your current tracking setup.