Expense Tracker
"Track what the team eats. Predict what comes next."
Table of Contents
What it is
An internal team tool for collaboratively tracking shared daily food expenses. Teams share a daily budget, and this app lets members log morning snacks, lunch, and evening meals against a configurable per-person daily spending limit.
Built as a Progressive Web App with full offline capability, so expenses can be logged even without connectivity and sync when back online. Includes a TensorFlow.js LSTM model that predicts next day's spending by meal type.
How it works
Screenshots
Architecture Decisions
Why React + Vite instead of Next.js?
> Pure SPA with no SSR needs. Firebase is the backend. Vite optimizes the PWA build.
The app uses BrowserRouter with client-side routing only. There are zero server-side routes, zero SSR concerns. All data access is direct client-side Firestore calls — no API layer, no middleware, no server deployment. Next.js would add SSR/SSG complexity for zero benefit.
Vite's PWA plugin (vite-plugin-pwa) generates the service worker automatically with Workbox. This is critical for the offline-first architecture. TensorFlow.js is 3MB+ — Vite's tree-shaking and chunk optimization keep the bundle manageable.
> Tradeoff: No server-side rendering means no SEO. Acceptable for an internal tool behind Firebase Auth.
Why Firebase (Firestore + Auth + Analytics) instead of Supabase or custom backend?
> Auth + Database + Analytics in a single SDK. Zero server infrastructure.
Firebase Auth handles signInWithEmailAndPassword, createUserWithEmailAndPassword, and sendPasswordResetEmail entirely client-side. No server needed. Firestore provides direct client-side queries with range filters on date fields. Firebase Analytics gives native event tracking with zero extra setup.
The project is already provisioned with Auth domain, storage bucket, and measurement ID. The Workbox config specifically targets firestore.googleapis.com with NetworkFirst strategy, a deep Firebase-specific integration.
> Tradeoff: Firebase credentials are hardcoded in the source (not using env vars as README suggests). Acceptable for an internal tool not published to npm.
Why TensorFlow.js for predictions (client-side ML) instead of server-side?
> Data never leaves the device. Works offline. Zero server infrastructure. Runs in under 1 second.
The entire prediction pipeline runs client-side: tf.model.fit() trains on the last 30 days of meal data, then model.predict() forecasts the next 1-2 workdays. No server endpoint exists. This means:
- Privacy: Expense amounts and patterns never leave the user's device
- Offline: Predictions work without internet (critical for PWA)
- Performance: 100 epochs on ~25 training sequences runs in under 1 second on modern mobile devices
- Cost: Zero inference cost. No GPU server needed.
Model Architecture
Why these specific parameters?
- SEQUENCE_LENGTH = 5: One work week. Captures weekly patterns (e.g., Monday is always higher).
- FEATURES = 4: Three meal amounts + day-of-week. Day-of-week captures cyclical patterns.
- LSTM units = 32: Small but sufficient for 3 meal categories. Avoids overfitting on ~25 training samples.
- Dropout = 0.2: Prevents overfitting on the tiny dataset. With ~25 sequences, overfitting is the #1 risk.
- Normalization /1000: Indian food expenses range 50-500 INR. Dividing by 1000 brings values to 0.05-0.5, optimal for neural network training.
- 30-day window: Balances recency vs. data quantity. More than 30 days would include stale patterns.
- Auto-regressive: Each prediction feeds into the next input sequence, enabling multi-day forecasting.
- Weekend skip: Weekends have no expenses, so predicting them produces meaningless zeros.
Why IndexedDB + Workbox for offline instead of just localStorage?
> Two layers of offline support: IndexedDB for write-queue management, Workbox for API response caching.
IndexedDB (via idb): Stores expenses with structured indexes on userId and synced fields. The synced boolean implements an outbox pattern — expenses created offline are marked synced: false, then synced when connectivity returns. localStorage's 5MB limit, synchronous API, and lack of indexing make it unsuitable for this use case.
Workbox: The service worker uses NetworkFirst strategy for firestore.googleapis.com, caching up to 100 API responses for 1 week. This provides read-only offline access to previously fetched data. Preact caching handles static assets.
> Tradeoff: The offline write-queue (db.ts) is scaffolded but not fully wired to ExpenseForm.tsx, which still writes directly to Firestore. Background sync registration is incomplete.
Why exactly three meal categories (morning/lunch/evening)?
> Maps to the standard Indian corporate meal schedule.
The standard meal pattern is: breakfast/morning tea, lunch, and evening snacks/tea. This maps exactly to three expense periods. "Dinner" is not tracked because it happens after work hours — this is a company food expense tracker, not a personal finance app.
The three categories are consistent through the entire stack: TypeScript enum ('morning' | 'lunch' | 'evening'), Firestore queries, Chart.js datasets, TensorFlow.js input/output dimensions, and dashboard breakdowns.
Why weekday-only validation?
> The company provides meals Monday through Friday. Weekend entries would corrupt budget calculations.
Enforced at three levels: client-side validation in ExpenseForm (getDay() === 0 || 6 triggers an alert), week initialization in expenseService (only Mon-Fri), and ML model prediction (skips weekends). The weekly budget formula is: numberOfPeople * dailyLimitPerPerson * 5.
Why the email domain restriction?
> Security boundary for a corporate tool. Only company employees can access the expense system.
Enforced via an endsWith() check in SignupForm, with HTML5 pattern attribute for client-side validation. Firestore security rules (configured in Firebase Console) enforce this server-side too.
Why bottom nav on mobile, top nav on desktop?
> Mobile-first design following platform conventions. Bottom tabs for thumb reach on mobile.
Navigation.tsx uses: className="... fixed bottom-0 ... md:static md:bottom-auto". On mobile, a fixed bottom tab bar provides easy thumb access. On desktop, it moves to a static top navigation. The PWA manifest locks to orientation: 'portrait' and display: 'standalone', removing browser chrome for an app-like experience.
Features
- Authentication — restricted to corporate email addresses via Firebase Auth
- Expense CRUD — add, edit, delete expenses with date, meal type, and amount in INR. Weekday-only validation enforced
- Dashboard — three summary cards (today's remaining, weekly balance, weekly total), stacked line chart showing daily trends by meal type, and expandable daily breakdown with per-user details
- Settings — configure team size and daily per-person budget limit. Weekly budget auto-calculated as: people x daily limit x 5 weekdays
- ML expense prediction — TensorFlow.js LSTM model trained on last 30 days of data to predict next day's spending by meal type
- Offline-first PWA — Workbox service worker with precaching and runtime caching of Firestore API calls, IndexedDB for offline expense storage
- Analytics — Firebase Analytics tracking login, expense operations, settings changes, and PWA installs
Tech Stack
- Framework: React 18 + Vite + TypeScript — pure SPA, no SSR
- Styling: Tailwind CSS — utility-first, responsive mobile-first
- Backend: Firebase (Firestore + Auth + Analytics) — zero server infrastructure
- Charts: Chart.js + react-chartjs-2 — stacked line chart with per-meal datasets
- ML: TensorFlow.js — client-side LSTM inference, 32-unit model, 100 epochs
- Offline: IndexedDB via
idb+ Workbox viavite-plugin-pwa - Icons: Lucide React — lightweight, tree-shakeable
- Dates: date-fns — immutable, tree-shakeable date manipulation
Key Numbers
- ~1,930 lines of application code across 22 TypeScript/TSX files
- 32 LSTM units in the prediction model
- 5-day sliding window for training sequences (one work week)
- 30-day lookback for training data recency
- 100 epochs per training run, completes in under 1 second
- 3 meal categories — morning, lunch, evening
- 5 weekdays — weekends excluded from all calculations
- INR only — hardcoded Indian Rupee throughout
What I learned
- Client-side ML is viable for small models. TensorFlow.js LSTM trains in under 1 second on mobile.
- Offline-first is a spectrum. The IndexedDB scaffolding exists but the full sync orchestration is incomplete. Perfect is the enemy of shipped.
- Firebase Auth + Firestore eliminates entire backend categories. The tradeoff is vendor lock-in.
- Workbox's
NetworkFirststrategy for Firestore API is the right pattern: serve cached data when offline, fresh data when online. - The meal categorization system (morning/lunch/evening) is a domain-specific constraint that should have been configurable from day one.