← All work
Live · 2026  Case study

UW FUEL

A nutrition and fitness app for UW students. It turns the real campus dining menus into meal plans that fit your budget, logs workouts against a 1,300+ exercise library, and runs on API keys each student owns.

100+
students in the first three weeks
1,300+
exercises in the library
28.9k
lines of TypeScript
$0
per user for me to run
Role · Solo: product, design, and engineering Stack · Next.js 16 · React 19 · TypeScript · Tailwind v4 · Firebase · Cohere · Python + Playwright
Prototype
Mar 2026private, 24 commits
Rebuild
Jun 23, 2026
Latest
Oct 2026scraper; app on Sep 14
Code
28.9k lines19.1k TSX, 9.2k TS
Files
217
01 · Timeline

A one-page prototype, then a real app

UW FUEL went through two versions in about seven months. The prototype and the current scraper are in private repos, so those entries show dates without links.

  1. Prototype · UW Macros Planner
  2. Mar 8, 2026
    First version

    A Python scraper (Playwright against UW's dining order site, scheduled with APScheduler) that wrote menus into Firebase, with one large index.html as the whole front end.

    Private repo
  3. Mar – May 2026
    Iterating on one page

    24 commits through May 4, and 20 of them touch that single HTML file. The rebuild split it into a real app.

    Private repo24 commits
  4. UW FUEL · Next.js
  5. Jun 23, 2026
    Rebuilt as UW FUEL

    Started over in Next.js and TypeScript with three route groups (public pages, onboarding, and the signed-in app), typed Firebase hooks, the meal planner, the workout logger, and progress charts. A second commit that day sped up the workout log.

    GitHub143 files · +30,746 lines563eea1 ↗01cd4ee ↗
  6. Jul 2, 2026
    Setup guide and dining-plan tracking

    Added a step-by-step setup guide inside the app and tracking against your dining plan balance. Fixed a production bug and some viewport problems later the same day.

    GitHub44 files · +2,306 / −2993cc564c ↗8cddee4 ↗b990b0c ↗
  7. Jul 10, 2026
    Dorm pantry

    A pantry tab for food you keep in your room: search ingredients, scan items with your camera, and get recipe ideas from what you already have.

    GitHub18 files · +1,483 / −28d6990dd ↗
  8. Sep 12, 2026
    My Day, new sign-up, and a stricter planner

    The biggest change since launch. A new Today view lays out the day's meals on a route map, login and signup got reworked, and the planner gained fitDay.ts, the deterministic pass that enforces the budget. Two more commits that day added drag-to-reorder and meal ratings to plans, merged duplicate dining cards, and fixed the tutorial getting clipped.

    GitHub124 files · +5,630 / −1,4639f454a3 ↗8c47880 ↗eab5c58 ↗
  9. Sep 14, 2026
    Plans that follow your class schedule

    Meal planning can now follow your class schedule, plus an accessibility pass on the interface. A follow-up commit made plans reusable.

    GitHub14 files · +665 / −31826fbfc6 ↗a76ba5d ↗
  10. Oct 5, 2026
    Scraper moved to GitHub Actions

    Split the dining scraper into its own repo. It runs every night at midnight Pacific on GitHub Actions, with two cron entries so the time stays right through daylight saving changes.

    Private repo4 commits
02 · The problem

Eating well on a meal plan is harder than it should be

Dining dollars run out before the month does. You never quite know what's open, what fits your macros, or what something costs until you're in line. And general nutrition apps know nothing about UW's dining halls, so you end up guessing at every entry.

UW FUEL uses the real UW Housing and Food Services menu, with real prices and real hours, so the easy choice and the smart choice can be the same one. I also wanted it to cost me nothing per user, which shaped most of the architecture.

03 · How it works

Public data in one place, personal data wherever the student wants it

A Python scraper pulls the dining menus every night and writes them into a shared Firebase project, next to the exercise library. Everyone reads the same public data and nobody can write to it. Personal data (food logs, plans, workouts, settings) goes under the signed-in user's own node, and security rules keep each user inside their own subtree. A student who wants full ownership can connect their own Firebase project instead.

AI requests go straight from the browser to Cohere using the student's own key. There's no server of mine in the middle, so there's no shared quota and nothing for me to pay per request. The key is stored under the user's authenticated database node rather than in localStorage, which keeps it away from anything a cross-site script could read.

UW dining sitemenus · prices · hours
nightly
→
Python scraperGitHub Actions
→
Shared Firebasemenus · exercises
read-only
→
Next.js apptyped onValue hooks
Coherestudent's own key
Personal dataown node or own Firebase

The maintainer-owned part of the system only ever holds public data.

04 · The budget planner

The model suggests meals, and code enforces the budget

The planner's one rule: find food that fits the budget, and never bend the budget to fit a plan. The model gets your goal, your dietary preferences, your meal count, your hard cap, and today's menu with exact prices, and it's told to pick only from that list. Then a deterministic pass in fitDay.ts checks the result against real prices, because a prompt alone can't guarantee arithmetic.

While the plan is over budget, it takes the most expensive meal and tries the cheapest fix first. Add-ons go before anything else. Next it looks for a swap: the best-ranked item that fits the remaining room and is open at that meal's time. If nothing fits, that meal gets dropped. Once the plan is under the cap, it spends what's left on sides from the same dining location until you're close to your calorie target.

src/lib/planner/fitDay.ts
for (let pass = 0; pass < MAX_PASSES && totalCost() > budget + 1e-9 && drafts.length > 0; pass++) {
  let idx = 0;
  drafts.forEach((d, i) => { if (draftCost(d) > draftCost(drafts[idx])) idx = i; });
  const d = drafts[idx];

  if (d.addOns.length > 0) {                      // cheapest fix first
    used.delete(d.addOns.pop()!.key);
    continue;
  }

  const room = budget - (totalCost() - draftCost(d));
  const swap = bestOf(input.pool, score,
    (item) => item.price <= room && isOpen(item, clockToMinute(d.time)));
  if (swap) { d.main = swap; swaps++; continue; }   // swap for something that fits

  drafts.splice(idx, 1);                           // nothing fits: drop the meal
  droppedMeals++;
}

Simplified from fitDay.ts. The real version also tracks used items and has a fallback swap rule.

Whatever it changes, it says so. You'll see notes like "Swapped 2 pricier picks to stay under $12.00," and if the cap is too low to reach your calories it tells you how far short you are and suggests raising the cap. It never rescales a price to make a plan look like it fits.

05 · Learning what you like

Ratings that fade, and a penalty for repeats

Once plans could be rated, the planner needed to use those ratings without getting stuck on them. preferences.ts turns your history into a score for each food:

Ratings decay

"Loved" counts +1.0, "good" +0.4, and "bad" −1.2, and each rating loses half its weight every 45 days. A meal you hated in September doesn't haunt you forever.

Repeats get pushed down

Anything you ate in the last three days takes a growing penalty, capped at 0.9, so the same bowl doesn't show up four days running.

Skips count

Food you keep skipping over a 14-day window slides down the list, up to a cap of 0.6.

Favorite spots

Dining locations you go to most get a small boost, up to 0.3, so plans lean toward your usual spots.

06 · Smaller things that took real work

Time zones, offline writes, and other details

07 · What's next

Where it's going

Sign-up, onboarding, dining, planning, logging, and progress are done and in use. Next up are richer progress analytics, smarter routing between classes and dining halls, and demos for more of the exercise library.

Try it, or read the code.

AI features run on your own Cohere key.

More work