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.