A full-stack TypeScript monorepo simulating an end-to-end smart vending machine network across multiple university campuses — with real-time inventory management, sustainability tracking, and dynamic white-label theming per institution.
- Overview
- Feature Matrix
- System Architecture
- Tech Stack
- Database Schema
- API Reference
- Getting Started
- Project Structure
- Architectural Decisions
- Author
SnackStalker models a production-grade campus vending machine network across 5 universities (UMD, Georgia Tech, Ohio State, UCLA, UT Austin). It implements three distinct role-based access control tiers, a transactional vend engine with row-level locking, a geospatial campus map, and a carbon-footprint sustainability analytics layer.
| Dimension | Details |
|---|---|
| Universities | 5 (UMD, Georgia Tech, Ohio State, UCLA, UT Austin) |
| Buildings seeded | ~40 across all campuses |
| Machines seeded | 29 (UMD) + per-university machines |
| Snack catalog | 25 SKUs with carbon footprint data |
| Vending slots per machine | 25 (5×5 grid, A1–E5) |
| User roles | student, admin, super_admin |
- JWT-authenticated login/registration by email or directory ID
- Interactive Leaflet campus map with GeoJSON building polygons
- Click-through flow: Building → Machine selector → 5×5 vending grid
- QR payment simulation with countdown dialog
- Vend animation — tactile CSS snack-drop sequence
- Personal carbon footprint dashboard (kg CO₂e, tree/miles equivalents)
- Personalized low-carbon snack suggestions
- Batch inventory stock updates with full audit trail (
restock_events) - Real-time transaction log (last 500 events)
- Analytics charts: revenue, top-selling SKUs, total dispenses (Recharts)
- Cross-machine inventory health view
- Aggregate sustainability leaderboard across all universities
- Cross-institution carbon metrics and per-student rankings
- White-label theming — CSS custom properties injected at runtime per university (colors, mascot, coordinates)
- Full light/dark mode toggle via
ThemeContext - RTK Query cache invalidation — UI auto-refreshes after vend or restock
┌─────────────────────────────────────────────────────────────────┐
│ Browser (React 19) │
│ │
│ Redux Store (RTK Query) ←→ React Router v7 ←→ Tailwind CSS │
│ │ │
│ Cache Tags: [Slots, Transactions, Sustainability] │
└────────────────────┬────────────────────────────────────────────┘
│ /api/v1/* (Vite proxy → :3001)
▼
┌─────────────────────────────────────────────────────────────────┐
│ Express 5 (Node.js / ESM) │
│ │
│ /auth /buildings /machines /vend /inventory │
│ /transactions /universities /sustainability │
│ │
│ Vend route: BEGIN → SELECT FOR UPDATE → DECREMENT → COMMIT │
└────────────────────┬────────────────────────────────────────────┘
│ pg.Pool (no ORM)
▼
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL 15+ │
│ │
│ 8 tables · 4 analytics views · SERIAL + UUID PKs │
│ Row-level locking on vend transactions │
└─────────────────────────────────────────────────────────────────┘
| Layer | Technology | Notes |
|---|---|---|
| Language | TypeScript 5.x | End-to-end typed (frontend + backend) |
| Frontend framework | React 19 + Vite 7 | ESM, HMR |
| State management | Redux Toolkit + RTK Query | Cache tags for auto-invalidation |
| Routing | React Router v7 | Client-side SPA routing |
| Styling | Tailwind CSS v4 | CSS custom props for university theming |
| Maps | Leaflet + React-Leaflet | GeoJSON polygon building overlays |
| Charts | Recharts | Admin analytics tab |
| QR codes | qrcode.react | Simulated payment flow |
| Backend | Express 5 (Node.js, ESM) | REST API on port 3001 |
| Database | PostgreSQL | Raw pg pool — no ORM |
| Auth | bcryptjs | Password hashing; session via Redux localStorage |
| Build | Vite (frontend) + tsc (backend) | Separate build pipelines |
| Linting | ESLint 9 + typescript-eslint | Shared config per workspace |
universities ──┬── users
│ └── (role: student | admin | super_admin)
└── buildings ──── vending_machines ──── vending_slots
│
snacks ────────────────────────────────────────────────── slots
└── carbon_kg_per_unit │
transactions
restock_events
| Table | Description |
|---|---|
universities |
5 institutions with branding colors, coordinates, zoom |
users |
Role-based accounts linked to a university |
buildings |
GeoJSON polygon per building, linked to university |
vending_machines |
Machine per building with floor location |
snacks |
25-item catalog with emoji, color, category, and carbon_kg_per_unit |
vending_slots |
5×5 grid per machine (A1–E5); current/max stock, price |
transactions |
Immutable vend audit log (denormalized for analytics) |
restock_events |
Audit trail for every admin stock change |
4 PostgreSQL views: building_inventory_view, snack_summary_view, building_snack_summary_view, building_dispensed_summary_view
| Method | Route | Role | Description |
|---|---|---|---|
POST |
/auth/login |
All | Authenticate user, return university object |
POST |
/auth/register |
Public | Register new student account |
GET |
/buildings |
All | List buildings filtered by universityId |
GET |
/buildings/:id/machines |
All | Get machines in a building |
GET |
/machines/:id/slots |
All | Get 5×5 slot grid for a machine |
POST |
/vend |
Student | Atomic vend: lock slot → decrement stock → log transaction |
PATCH |
/inventory/stock |
Admin | Batch stock update with restock event audit |
GET |
/transactions |
Admin | Last 500 transactions (optionally filtered by userId) |
GET |
/sustainability/leaderboard |
All | Per-university carbon leaderboard |
GET |
/sustainability/campus-stats |
Admin | Campus-wide aggregate carbon metrics |
GET |
/sustainability/all-universities |
Super-Admin | Cross-institution sustainability rollup |
- Node.js 20+
- PostgreSQL 15+
- npm 10+
git clone https://github.com/SriRammSS/snack-stalker.git
cd snack-stalker
# Install server dependencies
cd apps/server && npm install
# Install web dependencies
cd ../web && npm install# Create the database
createdb snack_stalker
# Run migrations in order
psql -d snack_stalker -f database/001_schema.sql
psql -d snack_stalker -f database/002_seed.sql
psql -d snack_stalker -f database/003_views_and_counters.sql
psql -d snack_stalker -f database/004_add_universities.sql
psql -d snack_stalker -f database/005_superadmin_universal.sql
psql -d snack_stalker -f database/006_add_student_seed.sql
psql -d snack_stalker -f database/007_add_carbon_to_snacks.sqlCreate apps/server/.env:
DATABASE_URL=postgresql://postgres:<password>@localhost:5432/snack_stalker
PORT=3001# Terminal 1 — Backend
cd apps/server && npm run dev
# Terminal 2 — Frontend
cd apps/web && npm run devFrontend available at http://localhost:5173 · API at http://localhost:3001
snack_stocker/
├── apps/
│ ├── server/ # Express 5 REST API
│ │ └── src/
│ │ ├── index.ts # App entry — route registration, server start
│ │ ├── db.ts # pg.Pool singleton
│ │ └── routes/ # auth, buildings, machines, vend, inventory,
│ │ # transactions, universities, sustainability
│ └── web/ # React 19 SPA
│ └── src/
│ ├── routes/ # Login, StudentDashboard, AdminDashboard,
│ │ # MapView, SustainabilityDashboard
│ ├── components/ # CampusMap, VendingMachineModal, VendingGrid,
│ │ # PaymentQRDialog, VendAnimation, admin/*
│ ├── context/ # UniversityContext (CSS theming), ThemeContext
│ └── store/ # Redux store, RTK Query apiSlice, auth/vend slices
└── database/ # Ordered PostgreSQL migration files (001–007)
| Decision | Rationale |
|---|---|
| No ORM | Raw pg pool gives full control over query shape and enables SELECT FOR UPDATE row locks on the vend path — critical for preventing oversell under concurrent load |
| RTK Query cache tags | Slots, Transactions, Sustainability tags auto-invalidate after a vend or restock, eliminating manual refetch calls |
| CSS custom properties for theming | UniversityContext sets --color-uni-primary etc. on :root at login time — the entire UI re-themes without a rebuild or page reload |
| Multi-phase vend UX | browsing → confirming → QR payment → animation → API commit makes the experience feel physical; the DB write fires only after the animation completes |
| Denormalized transactions table | Analytics queries (revenue, top SKUs, dispense counts) run against a flat log rather than joins across 4 tables — reduces query complexity for read-heavy dashboards |
Sri Ramm Sekar Sasirekha