AI fitness app admin panel
The daily operations console for a calorie-tracking app with 20K+ users: users, subscriptions, food and ingredient databases, exercise data, app configuration and usage analytics.
- Role
- Full-stack engineer
- Status
- In production
- Client
- AI calorie-tracking app
- Next.js
- React
- TypeScript
- Firebase (Firestore)
- Chakra UI

The problem
- Context
- The operations console sits directly on the live database of a calorie-tracking app with 20K+ users. It is the only window the team has into users, subscriptions, the food, ingredient and exercise catalogues, app settings, revenue and running costs, and it reads the app's own production data rather than a separate reporting copy.
- Constraints
- The database bills per document read, and the biggest collections (food logs and subscription events) are large. The same records had been written over time by the mobile app, a billing webhook and backfills, in different shapes. Some capabilities, like deploying indexes or enabling the billing export, depend on switches only the app owner controls.
- What was at stake
- A careless view can spend tens of thousands of reads in one click. A wrong assumption about data shape makes a number quietly lie, such as active trials showing as zero. And because server actions can be called directly, a missing role check is a real privilege-escalation path.
What I built
Cost control
Read cost shown before spending it
The food-trends view estimates its cost with cheap counts first and labels its button with the number of reads before anything runs. Results are kept for a day, a forced rebuild is limited to once every five minutes, and the cheaper indexed path is used automatically once the owner deploys that index.
Expensive counts cached
Counting every user took about two seconds and re-ran on each page change, so counts are cached for 60 seconds per query, and audience-tier counts for 12 hours.
Ranges that cannot run away
Date ranges sent from the browser are snapped to the options the interface offers, capped at a year, and cached per calendar month, with bounded caches, so nobody can trigger unbounded billable reads by changing a parameter.
Numbers you can trust
Unknown is shown as unknown, never zero
Usage counters were added to the app later, so older accounts do not have them. A missing counter, or a read that failed, is shown as unknown rather than zero, so the console never claims someone never used a feature.
Meals counted from the right source
Free users who logged meals by search appeared to have logged nothing, because the summary counters only moved on other paths. Meal counts now come from the per-day logs themselves.
Subscription dates in two formats
The mobile app and the billing webhook stored dates in two different formats, and a database range query silently skipped one of them, which is how active trials once read as zero. Subscriptions are now read and judged in memory.
One source for revenue
Revenue is calculated from the app's stored billing events, with one shared list feeding both totals and charts so they cannot disagree. A failed read shows no data instead of a misleading $0.00.
Access control
A role check in every action
Every server action checks the caller's role itself, using identity rebuilt from a verified token on each request. Middleware alone would let an editor call an admin-only action from any page.
Hardened sign-in
Admins sign in with bcrypt-hashed passwords and receive a token whose algorithm is pinned on both signing and checking. Every failed sign-in returns the same message, so admin addresses cannot be discovered.
Operations
Every bill on one page
A costs page brings together model, cloud, advertising and analytics spend next to revenue. Each source that cannot answer says why (for example, an export not switched on) instead of reporting zero, and all integrations are read-only by design.
Account deletion that spares shared data
Deleting an account removes the user's data and their own files, but never the shared food and exercise images every user relies on.
Indexes kept as a superset
The index file holds more than 170 composite indexes, kept as a superset of production, because the deploy tool offers to delete any index missing from the file.
Tell me what you’re building and where it’s stuck.
I’ll tell you the cleanest path forward, including if it’s “don’t build that.”
Or write tocontact@alihassan.dev
