all_articles()
FirebaseFirestoreReal-TimeNext.jsFull-Stack
February 20, 20257 min read

Real-Time Inventory Updates with Firebase Firestore: Patterns That Actually Scale

How we built real-time inventory sync for Templora.in using Firestore's onSnapshot listener, handled race conditions with transactions, and kept costs from exploding.

The Challenge: Inventory That Lies

E-commerce without real-time inventory is a support ticket waiting to happen. On Templora.in, we needed product counts to update the moment an order was placed — across every open browser tab, on every device.

Firebase Firestore's real-time listeners were the obvious answer. What wasn't obvious was how to avoid the pitfalls.

The Basic Pattern

import { doc, onSnapshot } from 'firebase/firestore'

function useProductStock(productId: string) {

const [stock, setStock] = useState<number | null>(null)

useEffect(() => {

const unsub = onSnapshot(

doc(db, 'products', productId),

(snap) => setStock(snap.data()?.stock ?? 0),

(err) => console.error('Firestore error:', err)

)

return unsub // cleanup on unmount

}, [productId])

return stock

}

This works. But it doesn't scale when you have 50 products on a page — you'd open 50 connections.

The Collection Pattern

// Listen to the whole collection (or a query) — one connection

const q = query(collection(db, 'products'), where('active', '==', true))

onSnapshot(q, (snapshot) => {

snapshot.docChanges().forEach((change) => {

if (change.type === 'modified') {

updateLocalStockMap(change.doc.id, change.doc.data().stock)

}

})

})

One listener, delta updates, handles hundreds of products.

Preventing Race Conditions with Transactions

The dangerous moment: two customers buy the last unit simultaneously.

import { runTransaction, doc } from 'firebase/firestore'

async function purchaseItem(productId: string, quantity: number) {

await runTransaction(db, async (tx) => {

const ref = doc(db, 'products', productId)

const snap = await tx.get(ref)

const current = snap.data()?.stock ?? 0

if (current < quantity) throw new Error('Out of stock')

tx.update(ref, { stock: current - quantity })

})

}

Firestore transactions are ACID-compliant. If two transactions conflict, one retries automatically.

Cost Control

Firestore charges per read/write. With naive listeners, you can burn through the free tier fast.

Our rules:

  • Query, don't listen globally — scope listeners to the current page's products
  • Detach on route change — always call the unsubscribe function
  • Use Security Rules as your last defence — validate stock server-side before charging

The result: real-time inventory on Templora.in at under $5/month for our current traffic.