Pantri: Building the Household App I Actually Wanted to Use
A deep look at the stack and design decisions behind Pantri — a household PWA for shopping lists, pantry tracking, and AI-assisted meal planning.
There’s a specific kind of frustration that comes from standing in a grocery store aisle, staring at a half-empty fridge photo you took yesterday, wondering if you already have soy sauce at home. Multiply that by two people, a rotating pantry, and four different apps to track it all, and you have the problem Pantri was built to solve.
The Problem With Every Existing Tool
The household management app space is oddly fragmented. There are solid grocery list apps. There are recipe apps. There are pantry trackers. A few apps try to bridge two of those three. None of them do all three well, and almost none of them answer the question that comes up every evening: what can we actually make for dinner?
Beyond the feature gap, the bigger issue is the experience. Most free grocery apps are covered in ads. Premium tiers gate features that feel like they should be baseline. And the handful of apps that try to go end-to-end are either so complex they require onboarding or so simple they’re just glorified checklists.
I wanted something that felt like a well-built iOS app (fast, focused, clean) but worked on any device in a browser. No ads. No freemium paywalls. A first-class experience that the household actually wants to use.
What Pantri Does
Pantri covers three interconnected workflows.
Shopping lists that are context-aware. Items have quantities, prices, categories, and live pantry status. Checking an item “in basket” is a separate state from “checked out.” When a shop is done and you complete the trip, all the checked-out items are batch-added to the pantry automatically. You don’t re-enter anything.
Pantry inventory that reflects reality. Stock bars, low-stock alerts, expiry tracking, and a restock suggestions panel that surfaces ingredients you cook with frequently but are running low on. The pantry is the source of truth the rest of the app reads from.
Recipes with actual pantry awareness. Every ingredient on a recipe detail page shows a badge (IN STOCK, LOW, or MISSING) against your live pantry. “Mark as Cooked” shows exactly which pantry quantities will be decremented before you confirm, then does it in one batch. Add missing ingredients directly to the shopping list with one tap. Import from any recipe URL that publishes structured JSON-LD data: AllRecipes, BBC Good Food, NYT Cooking, and most food blogs.
On top of all three: AI meal suggestions, a weekly meal planner, and barcode scanning.
The Tech Stack
I kept the stack small and didn’t reach for anything I didn’t need.
| Layer | Choice |
|---|---|
| Framework | React 19 + Vite |
| Backend / Auth / DB | Supabase (PostgreSQL, Auth, Realtime, Edge Functions) |
| Styling | Tailwind CSS v4 + shadcn/ui (Neon Protocol) |
| Client state | Zustand + TanStack Query |
| Routing | TanStack Router |
| AI | Supabase Edge Functions -> Groq (Llama 3.3 70B) |
| Hosting | Netlify |
Supabase does the heavy lifting. PostgreSQL handles all data with Row-Level Security policies that enforce household scoping on every table, so no query can return data from another household regardless of what the client sends. Auth is magic link or Google OAuth, and household invites go out by email. The real-time layer keeps shopping lists synced across devices without any polling.
Tailwind v4 ships CSS-first configuration, which is a slightly awkward fit with shadcn/ui since the component library’s tooling expects a tailwind.config.js. Getting them working together meant mapping shadcn’s CSS variables directly inside Tailwind’s @theme directive rather than going through the legacy plugin pipeline. Worth knowing if you’re trying the same combination.
The AI Layer
All AI features run through a single Supabase Edge Function (ai-suggest) that proxies to Groq’s Llama API. Groq’s free tier is fast enough for household scale, and the API key stays server-side in Edge Function secrets.
suggest-meal is the core “What’s for dinner?” feature. It gets the full recipe library annotated with pantry coverage, cook history (days since last cooked, total cook count), favorite status, and course type. It returns a single composed meal: a main, zero to two sides, a display name like “Chicken Tikka Night,” and a one-sentence reason. The pantry-filtered variant pre-filters to recipes where all ingredients are in stock before the AI call, so the model isn’t burning context reasoning about things you can’t make.
plan-week plans a full week of dinners in one shot. The model gets the recipe library, pantry, saved meal combos, existing calendar events, and user constraints (which evenings to plan, how many days to avoid repeating a recipe, free-text notes) and returns a per-day plan. Doing it in one call means the model can balance variety across all seven days instead of picking each day greedily. Confirmed plans write calendar events and log all recipes to recipe_occurrences.
nl-add and batch-pantry are natural language parsers for the shopping list and pantry. Type “oat milk, 2 dozen eggs, sourdough” and get back structured items with quantities and units. Describe a grocery haul in a sentence and have it added to the pantry all at once.
parse-recipe takes anything: a blog post, a screenshot OCR, a handwritten card. It returns a structured recipe with ingredients (quantities, units) and steps. Informal quantities like “a handful” and “2-3 cloves” get normalized as best as it can.
Design Decisions
Zod as the single source of truth
Every data shape in the app is defined as a Zod schema. TypeScript types are inferred from the schema, not written separately. Validation logic and type safety share one definition, so there’s no drift between what the form accepts and what the database receives. At system boundaries (edge functions, external APIs), unknown gets asserted with a Zod parse rather than a cast.
Optimistic UI on shopping list interactions
Checking, unchecking, reordering, and adding items all update immediately via TanStack Query’s onMutate / onError pattern. The mutation fires against Supabase in the background. If it fails, the cache rolls back. The list never hangs on a network round trip, which matters when you’re in a store with spotty signal.
Offline reads via query persistence
TanStack Query’s persistQueryClient middleware serializes the query cache to localStorage. Combined with Zustand’s persist middleware for auth and household state, the app loads fully from cached data when offline, with a sticky banner that reflects network state via window.online/offline events. No custom service worker caching needed beyond the default Vite PWA plugin.
Cook history as the AI feedback loop
Pantri tracks recipe_occurrences. Every time a recipe is marked as cooked or planned, a row is written with occurred_at and source (cooked vs. planned). That data feeds the AI as days_since_cooked and cook_count annotations, which lets the model penalize recent repeats and surface underused recipes. It also powers the recipe analytics page: variety score, rut detection, never-tried count, top-5 most cooked.
Barcode scanning without native wrappers
The barcode scanner uses @zxing/browser, a pure in-browser WebRTC camera pipeline with no native wrapper. EAN-13 / UPC-A codes resolve against the Open Food Facts API (free, no key required). Product name, category, and unit drop straight into the shopping list or pantry haul flow. If the barcode isn’t in the database, the user gets prompted to name the item manually so scanning never dead-ends.
The Design System
Pantri uses a custom design system called Neon Protocol, built from a baseline set of Material Design tokens and extended heavily by hand. It’s dark-first: deep background (#0d141b), progressively lighter surfaces for cards and modals, cyan accent (#00ded2) for interactive state and confirmation actions.
JetBrains Mono handles data-dense text: quantities, labels, stock counts, status indicators. Inter handles everything else. The contrast between the two creates a readable information hierarchy without leaning on color or size to do all the work.
Focus states use a 1px cyan border with a low-opacity glow. No drop shadows. Depth is handled through tonal layering.
What’s Next
Cook history, shopping history, pantry state, and recipe annotations are all being collected now. The next things I want to build are price prediction, expiry-based restock nudges, and cross-household recipe sharing.
One thing the AI features don’t solve is cold start. A fresh install has an empty pantry and no recipe library, so suggestion quality is low until you’ve built up some history. In practice, batch-pantry’s natural language input and the barcode scanner make the initial onboarding pretty quick (a full haul takes a few minutes, not an evening), but it’s still friction on first run.
The core question, “what’s for dinner?”, has an answer now. The work ahead is making it faster and more personal over time.
If you want to dig into the code, the repo is at github.com/ekahle2/pantri.