Real-Time Stock Visibility Across Multiple Warehouses
It’s 2:47 PM on a Friday. Your Shopify store just sold the last unit of your best-selling product. Except it didn’t—because your Amazon listing still shows 3 in stock, and by 2:52 PM you’ve oversold twice. Now you’re choosing between cancelled orders, angry customers, or expensive air freight from your supplier.
This scenario plays out thousands of times daily across ecommerce businesses. Despite investing in “inventory management systems,” stock visibility remains a constant pain point. The problem isn’t lack of technology—it’s the architecture of that technology.
The Visibility Gap: Why Traditional Systems Fail
Most inventory management systems were designed for a simpler era:
- Single warehouse
- Single sales channel
- Batch updates (nightly sync)
- Manual counts as the source of truth
Modern ecommerce operates differently:
- Multiple warehouses (your own + 3PL partners)
- Multiple sales channels (website, marketplaces, wholesale)
- Continuous transactions (orders every minute)
- Inventory in multiple states (available, reserved, in-transit, being picked)
Traditional systems can’t bridge this gap because they treat inventory as a static number rather than a dynamic state.
The Five States of Inventory
To achieve true visibility, you need to track inventory through its lifecycle:
State 1: On-Hand
Physical units in the warehouse. This is what you’d count if you walked the aisles.
On-Hand: 100 units at Warehouse A
50 units at Warehouse B
Total On-Hand: 150 units
State 2: Allocated
Units committed to orders but not yet picked. These should not be sold again.
On-Hand: 150 units
Allocated:
- Order #1234: 10 units (Warehouse A)
- Order #1235: 5 units (Warehouse B)
Total Allocated: 15 units
State 3: Reserved
Units held for non-order purposes: pending transfers, promotional holds, quality review.
Reserved:
- Transfer to Warehouse B: 20 units
- Quality Hold: 5 units
Total Reserved: 25 units
State 4: In-Transit
Units being received or transferred. These exist but aren’t yet available.
In-Transit:
- PO #789 arriving tomorrow: 200 units
- Transfer from Warehouse C: 30 units
Total In-Transit: 230 units
State 5: Available-to-Promise (ATP)
The number you can actually sell right now:
ATP = On-Hand - Allocated - Reserved
ATP = 150 - 15 - 25 = 110 units
Most overselling happens because systems show On-Hand (150) instead of ATP (110).
The Multi-Warehouse Complexity
With multiple warehouses, each location has its own inventory states:
Warehouse A (Brisbane):
On-Hand: 100
Allocated: 10
Reserved: 20
ATP: 70
Warehouse B (Sydney):
On-Hand: 50
Allocated: 5
Reserved: 5
ATP: 40
Company Total:
On-Hand: 150
ATP: 110
But ATP isn’t simply additive when selling across channels. Consider:
- A Sydney customer should see 40 units available (ship from nearest warehouse)
- A Brisbane customer should see 70 units available
- Nationwide availability might be 110 units (either warehouse can ship)
Your sales channels need location-aware ATP, not just a total.
The Channel Synchronisation Problem
Each sales channel has its own inventory number:
Your System (source of truth): ATP = 110
Channel Inventory Records:
- Shopify: 112 (stale by 2 sales)
- Amazon: 115 (stale by 5 sales)
- eBay: 108 (actually accurate)
- Wholesale Portal: 110 (accurate)
The gap between your system and channel records is the sync lag. During high-velocity periods, this lag causes overselling.
The Sync Lag Math
If you sell 10 units per hour and sync every 15 minutes:
- Maximum drift: 2.5 units between syncs
- During a flash sale (100 units/hour): 25 units drift
That’s 25 potential oversells in 15 minutes.
Real-Time Architecture: Event-Driven Inventory
The solution is replacing batch syncs with event-driven updates:
Stock Movement Event → Event Bus → Channel Update
→ ATP Recalculation
→ Dashboard Update
→ Alert Check
Every inventory-affecting action publishes an event:
// Events that affect inventory state
{
type: 'ORDER_PLACED',
sku: 'WIDGET-001',
quantity: 5,
warehouse: 'warehouse_a',
timestamp: '2026-01-26T14:47:00Z'
}
{
type: 'ORDER_PICKED',
sku: 'WIDGET-001',
quantity: 5,
warehouse: 'warehouse_a',
timestamp: '2026-01-26T15:02:00Z'
}
{
type: 'STOCK_RECEIVED',
sku: 'WIDGET-001',
quantity: 200,
warehouse: 'warehouse_a',
purchaseOrder: 'PO-789',
timestamp: '2026-01-26T16:30:00Z'
}
Firestore Real-Time Implementation
Using Firestore’s real-time listeners, channel adapters receive instant updates:
// ATP document structure
// /businessdata/{clientId}/atp/{sku}
{
sku: 'WIDGET-001',
totalATP: 110,
byWarehouse: {
'warehouse_a': 70,
'warehouse_b': 40
},
byChannel: {
'shopify': 110, // Can ship from any warehouse
'amazon_au': 40, // Only Sydney FBA eligible
'wholesale': 110
},
lastUpdated: Timestamp,
pendingInbound: 200,
nextInboundDate: '2026-01-27'
}
Channel adapters listen for changes:
// Shopify channel adapter
db.collection('businessdata/client_a/atp')
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach(async (change) => {
const atpData = change.doc.data();
await updateShopifyInventory(atpData.sku, atpData.byChannel.shopify);
});
});
Channel updates happen within seconds of stock movements, not minutes or hours.
The Allocation Race Condition
Even with real-time sync, a race condition exists:
T+0ms: ATP = 1 unit
T+0ms: Customer A views product page, sees "1 in stock"
T+0ms: Customer B views product page, sees "1 in stock"
T+50ms: Customer A adds to cart
T+100ms: Customer B adds to cart
T+200ms: Customer A completes checkout → ATP = 0
T+250ms: Customer B completes checkout → OVERSOLD
Both customers saw accurate inventory, but the 200ms between decisions allowed an oversell.
Solution: Soft Allocation on Cart Add
Reserve inventory when added to cart, not at checkout:
async function addToCart(userId, sku, quantity) {
const result = await db.runTransaction(async (transaction) => {
const atpDoc = await transaction.get(atpRef);
const currentATP = atpDoc.data().totalATP;
if (currentATP < quantity) {
throw new Error('Insufficient stock');
}
// Create soft allocation
transaction.set(softAllocationRef, {
userId,
sku,
quantity,
expiresAt: new Date(Date.now() + 15 * 60 * 1000), // 15 min hold
status: 'held'
});
// Reduce ATP
transaction.update(atpRef, {
totalATP: currentATP - quantity,
softAllocated: FieldValue.increment(quantity)
});
return { success: true };
});
return result;
}
Soft allocations expire if checkout isn’t completed, returning stock to ATP.
Multi-Warehouse Fulfilment Logic
When ATP exists across multiple warehouses, order routing becomes complex:
Strategy 1: Nearest Warehouse
Ship from the closest warehouse to the customer:
function selectWarehouse(order, warehouseATPs) {
const customerZone = getDeliveryZone(order.shippingAddress);
// Filter warehouses with sufficient stock
const viable = warehouseATPs
.filter(w => w.atp >= order.quantity)
.map(w => ({
...w,
distance: calculateDistance(w.location, customerZone)
}))
.sort((a, b) => a.distance - b.distance);
return viable[0] || null;
}
Strategy 2: Inventory Balancing
Distribute orders to balance stock levels:
function selectWarehouse(order, warehouseATPs) {
const viable = warehouseATPs.filter(w => w.atp >= order.quantity);
// Prefer warehouse with highest stock to balance inventory
return viable.sort((a, b) => b.atp - a.atp)[0] || null;
}
Strategy 3: Cost Optimisation
Factor in shipping costs:
async function selectWarehouse(order, warehouseATPs) {
const options = await Promise.all(
warehouseATPs
.filter(w => w.atp >= order.quantity)
.map(async (w) => {
const shippingCost = await getShippingQuote(w, order.shippingAddress);
return { ...w, shippingCost };
})
);
return options.sort((a, b) => a.shippingCost - b.shippingCost)[0] || null;
}
Strategy 4: Split Shipment
When no single warehouse can fulfil the order:
function planSplitShipment(order, warehouseATPs) {
let remaining = order.quantity;
const shipments = [];
// Sort by ATP descending
const sorted = [...warehouseATPs].sort((a, b) => b.atp - a.atp);
for (const warehouse of sorted) {
if (remaining <= 0) break;
const shipQty = Math.min(warehouse.atp, remaining);
if (shipQty > 0) {
shipments.push({
warehouse: warehouse.id,
quantity: shipQty
});
remaining -= shipQty;
}
}
if (remaining > 0) {
return { success: false, shortfall: remaining };
}
return { success: true, shipments };
}
Channel-Specific ATP Rules
Different channels need different ATP calculations:
Amazon FBA
Only show stock physically in Amazon’s warehouses:
const amazonATP = warehouseATPs
.filter(w => w.type === 'amazon_fba')
.reduce((sum, w) => sum + w.atp, 0);
Marketplace with Long Lead Time
Include in-transit stock for channels that accept longer delivery windows:
const wholesaleATP = totalATP + pendingInbound.filter(po =>
po.expectedDate <= addDays(today, 7)
).reduce((sum, po) => sum + po.quantity, 0);
Pre-Order Channel
Show future inventory for pre-order campaigns:
const preorderATP = pendingInbound
.filter(po => po.status === 'confirmed')
.reduce((sum, po) => sum + po.quantity, 0);
Low Stock Alerts and Reorder Points
Real-time visibility enables proactive inventory management:
// Firestore trigger on ATP changes
exports.checkReorderPoint = functions.firestore
.document('businessdata/{clientId}/atp/{sku}')
.onUpdate(async (change, context) => {
const newData = change.after.data();
const sku = context.params.sku;
const clientId = context.params.clientId;
// Get reorder configuration
const config = await getReorderConfig(clientId, sku);
if (newData.totalATP <= config.reorderPoint) {
await createReorderAlert({
clientId,
sku,
currentATP: newData.totalATP,
reorderPoint: config.reorderPoint,
suggestedQuantity: config.reorderQuantity,
severity: newData.totalATP <= config.safetyStock ? 'critical' : 'warning'
});
}
});
Audit Trail for Stock Movements
Every ATP change must be traceable:
// Movement log entry
{
timestamp: '2026-01-26T14:47:00Z',
sku: 'WIDGET-001',
warehouse: 'warehouse_a',
movementType: 'ORDER_ALLOCATION',
quantityChange: -5,
atpBefore: 75,
atpAfter: 70,
reference: {
type: 'order',
id: 'ORD-12345'
},
userId: 'system', // or actual user for manual adjustments
metadata: {
orderChannel: 'shopify',
customerId: 'CUST-789'
}
}
This audit trail allows you to:
- Investigate discrepancies: “Why did ATP drop from 75 to 70?”
- Reconstruct historical state: “What was ATP at 2 PM yesterday?”
- Identify patterns: “Which channel drives the most volatility?”
Implementation Checklist
Phase 1: Foundation
- Implement ATP calculation (On-Hand - Allocated - Reserved)
- Create real-time ATP document per SKU
- Set up Firestore listeners for stock movements
- Build audit trail logging
Phase 2: Multi-Warehouse
- Track ATP per warehouse
- Implement warehouse selection strategies
- Handle split shipment scenarios
- Create cross-warehouse transfer workflows
Phase 3: Channel Integration
- Build channel adapters with real-time listeners
- Implement channel-specific ATP rules
- Add soft allocation on cart add
- Create reconciliation for channel sync failures
Phase 4: Intelligence
- Set up reorder point monitoring
- Build velocity tracking (sales rate per SKU/warehouse)
- Implement demand forecasting
- Create stock distribution recommendations
Measuring Success
Track these metrics to validate your real-time visibility implementation:
| Metric | Before | Target |
|---|---|---|
| Oversell rate | 2.3% of orders | Under 0.1% |
| Channel sync lag | 15 minutes | Under 30 seconds |
| Stock discrepancy rate | 4% of SKUs | Under 0.5% |
| Time to detect stockout | 4 hours | Under 1 minute |
| Order cancellation (stock) | 1.8% | Under 0.2% |
Conclusion
Real-time stock visibility isn’t a nice-to-have—it’s table stakes for modern ecommerce. The cost of overselling (refunds, reputation damage, marketplace penalties) far exceeds the investment in proper inventory architecture.
The key insights:
- ATP, not On-Hand: Sell what’s actually available, not what’s physically present.
- Event-driven, not batch: Push changes instantly, don’t wait for sync cycles.
- Location-aware: Different customers and channels need different ATP views.
- Allocation at cart: Reserve stock early to prevent race conditions.
- Complete audit trail: Every movement logged for investigation and compliance.
The technology exists. Firestore’s real-time capabilities, combined with event-driven architecture, can deliver sub-second stock visibility across warehouses and channels. The question is whether you’ll implement it before your next oversell becomes a viral customer complaint.
Need real-time inventory visibility across warehouses and channels? See how EQUOS delivers sub-second stock synchronisation for multi-location ecommerce operations.