Expiry Alerts

Food Tracker (React + Firebase)

Chapter 7 · Expiry Alerts

Chapter 6 finally protected real data with real rules. This chapter is the first payoff of having that data at all: surfacing which items are about to go to waste.

The On-Demand Query

import { collection, query, where, Timestamp, getDocs } from "firebase/firestore"; async function getExpiringSoonItems(daysAhead = 3) { const now = Timestamp.now(); const threshold = Timestamp.fromMillis(now.toMillis() + daysAhead * 24 * 60 * 60 * 1000); const q = query( collection(db, "items"), where("status", "==", "active"), where("expiryDate", "<=", threshold) ); const snapshot = await getDocs(q); return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); }

Filtering on status == "active" is technically redundant with Chapter 2's own "used items omit expiryDate entirely" design — a range comparison already excludes documents missing the field. It's included anyway as explicit, readable intent rather than relying on an implicit side effect of the data model to do the filtering silently.

The Composite Index Requirement

Run the query above for the first time, and Firestore throws an error rather than executing it — something like "The query requires an index." This isn't a mistake in the query; it's Firestore telling you the truth about how it works. A single-field query gets an automatic index for free. A query combining an equality filter on one field (status) with a range filter on a different field (expiryDate) needs a composite index, declared ahead of time — either by clicking the link Firestore's own error message provides (which pre-fills the exact index needed), or by committing a firestore.indexes.json file to source control so the same index gets created automatically in every environment.

SQL handles the equivalent query differently: an unindexed multi-column WHERE clause still runs — just slower, via a full table scan, with no error at all. Firestore refuses outright rather than running a query it can't serve efficiently at scale. Neither behavior is objectively better; they reflect two different philosophies about whether "it'll just be slow" is an acceptable default.

Rendering the Dashboard

function ExpiryDashboard() { const [expiring, setExpiring] = useState([]); useEffect(() => { getExpiringSoonItems(3).then(setExpiring); }, []); return (
    {expiring.map(item =>
  • {item.name} — expires {item.expiryDate.toDate().toLocaleDateString()}
  • )}
); }

Should Alerts Be Proactive? An Optional Scheduled Function

The dashboard above only tells anyone anything if they actually open the app. A genuinely proactive alert — a notification that arrives even when nobody's looking — needs code running on a schedule, independent of any user visiting the page. Firebase's onSchedule lets a Cloud Function run on a cron-style schedule with no server kept alive between runs:

const { onSchedule } = require("firebase-functions/v2/scheduler"); exports.dailyExpiryCheck = onSchedule("every day 08:00", async (event) => { const expiringItems = await getExpiringSoonItemsAdmin(3); // same query, Admin SDK // write a notification doc, or send via Firebase Cloud Messaging });

This is explicitly optional — a stretch feature, not the MVP. The on-demand dashboard query above is what this course actually requires; a scheduled proactive notification is a genuine enhancement worth attempting once the core app works, not a prerequisite for it.

What the Other Three Courses Would Need Instead

CourseHow it would run a daily proactive check
Food Tracker (FastAPI)A scheduler library (e.g. APScheduler) running inside a continuously-alive server process
Food Tracker (Django)Celery Beat, django-crontab, or an OS-level cron job, all requiring something to stay running
Food Tracker (React + Express)node-cron or a system crontab entry, again requiring a persistently-running process
Food Tracker (React + Firebase)onSchedule — no process kept running between invocations at all
The "no server you deploy yourself" idea, applied to time itself
Every sibling course needs something alive around the clock just to notice a date has passed — a whole server process exists partly to support one function that fires once a day. This course's scheduled function exists, compute-wise, only for the seconds it actually executes. It's the same architectural theme from Chapter 1, now showing up in the dimension of when code runs, not just where.
Testing a scheduled function without waiting a day
The Firebase emulator doesn't fire scheduled functions automatically on a real clock — invoke the underlying handler directly during development instead of waiting for 8:00 AM to roll around, and only rely on the actual schedule once deployed to production.
"The query requires an index" is not a syntax error
It's easy to misread that message as a mistake in how the query was written and start second-guessing the code. Read the error itself — Firestore includes a direct console link to auto-create the exact index the query needs. Following that link is almost always faster than debugging code that was never actually broken.

Where This Course Is Headed

Item history with live search (and Firestore's own honest limits on text search), marking items used, recipe lookup, Firebase Authentication, deployment, and a capstone.

Hands-On Exercises

Exercise 1

Explain what a Firestore composite index actually is, why this chapter's own query needs one, and how SQL handles the equivalent multi-column query differently.

📄 View solution
Exercise 2

Explain the difference between the on-demand dashboard query and the optional scheduled Cloud Function. What real problem does the scheduled function solve that the on-demand query, by itself, never can?

📄 View solution
Exercise 3

Explain the finding-box's claim that this course applies its "no server you deploy yourself" idea to time itself. What would each of the three sibling courses need to keep running just to check expiry dates once a day, and why doesn't this course need the equivalent?

📄 View solution

Chapter 7 Quick Reference

  • On-demand querywhere("status","==","active") + where("expiryDate","<=",threshold), required for this course
  • Composite index — required for multi-field filter combinations; Firestore refuses to run the query until one exists, unlike SQL's slower-but-runs approach
  • onSchedule — a scheduled Cloud Function, optional/stretch, for proactive daily checks
  • No persistent process needed — unlike the cron/Celery/node-cron setup every sibling course would require
  • This chapter's own throughline: "no server you deploy yourself" applies to scheduled/background work too, not just request-driven code
  • Next chapter: Item History & Live Search-as-You-Type